Search code examples
reactjsnode.jstypescriptmultipartform-datamulter

Why is my form data being sent as application/json in my POST request, and why is it returning a Bad Request error?


I have a form to add hotel in the database. I am using react-hook-form. In the form, I am asking images as an input as well. Upon submitting the form, the data from the form is converted into form data and then sent to the API.

This is where the conversion is happening and form is being submitted.

This is my API call:

const formMethods = useForm<HotelFormData>();
  //we are not gonna destruct the formMethods object here

  const { handleSubmit } = formMethods;

  const onSubmit = handleSubmit((data: HotelFormData) => {
    console.log(data);
    //We are going to send the data to the server, but first we need to convert the data to FormData because we have imageFiles in the data and we can not send it as a JSON object
    const formData = new FormData();
    formData.append("name", data.name);
    formData.append("city", data.city);
    formData.append("country", data.country);
    formData.append("description", data.description);
    formData.append("type", data.type);
    formData.append("pricePerNight", data.pricePerNight.toString());
    formData.append("starRating", data.starRating.toString());
    formData.append("adultCapacity", data.adultCapacity.toString());
    formData.append("childCapacity", data.childCapacity.toString());
    data.facilities.forEach((facility, index) => {
      formData.append(`facilities[${index}]`, facility);
    });
    // for (let i = 0; i < data.imageFiles.length; i++) {
    //   formData.append("imageFiles", data.imageFiles[i]);
    // }
    Array.from(data.imageFiles).forEach((imageFile) => {
      formData.append("imageFiles", imageFile);
    });
    onSave(formData);
    console.log(formData);
  });
export const addMyHotel = async (formData: FormData) => {
    console.log("formData",formData)
    const response = await fetch(`${BASE_URL}/api/my-hotels`, {
        method: "POST",
        credentials: "include", //to send the cookie
        body: formData,
        
    });
    const resBody = await response.json();

    if (!response.ok) {
        throw new Error(resBody.message);
    }
    return resBody;
};

This is my api/my-hotels:

import express from "express";
import { Request, Response } from "express";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import mime from "mime-types";
import { client } from "../index";
import multer from "multer";
import Hotel, { HotelType } from "../models/hotel";
import verifyToken from "../middleware/auth";
import { body, validationResult } from "express-validator";

const router = express.Router();

const storage = multer.memoryStorage();
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB
  },
});

//this will be the root
router.post(
  "/",
  verifyToken, //only logged in users can create hotels
  [
    body("name").notEmpty().withMessage("Name is required"),
    body("city").notEmpty().withMessage("City is required"),
    body("country").notEmpty().withMessage("Country is required"),
    body("description").notEmpty().withMessage("Description is required"),
    body("type").notEmpty().withMessage("Type is required"),
    body("pricePerNight")
      .notEmpty()
      .isNumeric()
      .withMessage("Price per night must be a number"),
    body("adultCapacity")
      .notEmpty()
      .isNumeric()
      .withMessage("Adult capacity must be a number"),
    body("childCapacity")
      .notEmpty()
      .isNumeric()
      .withMessage("Child capacity must be a number"),
    body("starRating")
      .notEmpty()
      .isNumeric()
      .withMessage("Star rating must be a number"),
    body("facilities")
      .notEmpty()
      .isArray()
      .withMessage("Facilities must be an array"),
  ],
  upload.array("imageFiles", 6),
  async (req: Request, res: Response) => {
    console.log("req body");
    console.log(req.body);
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res
        .status(400)
        .json({ message: "Bad Request", errors: errors. array() });
    }
    try {
      const files = req.files as Express.Multer.File[];
      const promises = files.map(async (file) => {
        const fileExt = mime.extension(file.mimetype);
        console.log(file);
        // const fileName = file.originalname;
        if (!fileExt) {
          throw new Error("Invalid file type");
        }

        const newFileName = `${Date.now()}`;

        const params = {
          Bucket: process.env.S3_BUCKET as string,
          Key: `${Date.now()}.${fileExt}`,
          Body: file.buffer,
          ContentType: file.mimetype,
        };
        await client.send(new PutObjectCommand(params));
        const link = `https://${process.env.S3_BUCKET_NAME}.${process.env.S3_REGION}.amazonaws.com/${newFileName}`;
        return link;
      });
      const imageUrls = await Promise.all(promises);

      const newHotel: HotelType = req.body; //some of the fields will already be filled by the req body
      newHotel.imageUrls = imageUrls;
      newHotel.lastUpdated = new Date();
      newHotel.userId = req.userId; //this is taken from the auth token

      //save the hotel to the database
      const hotel = new Hotel(newHotel);
      await hotel.save();
      res.status(201).send(hotel); //201 is the status code for created

      res.status(201).json({ message: "Images uploaded successfully" });
    } catch (error) {
      console.log("error creating hotel", error);
      res.status(500).json({ message: "Internal server error" });
    }
  }
);

export default router;

Now, when I submit the form I get the following errors:

Bad Request - errors:

0: {type: "field", msg: "Name is required", path: "name", location: "body"}
1: {type: "field", msg: "City is required", path: "city", location: "body"}
2: {type: "field", msg: "Country is required", path: "country", location: "body"}
3: {type: "field", msg: "Description is required", path: "description", location: "body"}
4: {type: "field", msg: "Type is required", path: "type", location: "body"}
5: {type: "field", msg: "Invalid value", path: "pricePerNight", location: "body"}
6: {type: "field", msg: "Price per night must be a number", path: "pricePerNight", location: "body"}
7: {type: "field", msg: "Invalid value", path: "adultCapacity", location: "body"}
8: {type: "field", msg: "Adult capacity must be a number", path: "adultCapacity", location: "body"}
9: {type: "field", msg: "Invalid value", path: "childCapacity", location: "body"}
10: {type: "field", msg: "Child capacity must be a number", path: "childCapacity", location: "body"}
11: {type: "field", msg: "Invalid value", path: "starRating", location: "body"}
12: {type: "field", msg: "Star rating must be a number", path: "starRating", location: "body"}
13: {type: "field", msg: "Invalid value", path: "facilities", location: "body"}
14: {type: "field", msg: "Facilities must be an array", path: "facilities", location: "body"}

The content type in my headers is set to application/json; charset=utf-8.

This is the payload of the request:

name: My Hotel
city: Iwdj
country: Pdihuf
description: poiuytrewqasdfghjkllllllllllllllllkmnbvcxz
type: hotel
pricePerNight: 7
starRating: 3
adultCapacity: 8
childCapacity: 7
facilities[0]: free parking
facilities[1]: swimming pool
imageFiles: (binary)
imageFiles: (binary)

I tried to manually set the content type to multipart/formdata but then I got an error

Multipart: Boundary not found


Solution

  • As the request is of multipart/form-data content type, express validator cannot process it, because it runs before multer processes the request, so it receives empty body, hence every validation fails (it probably tries to process the request as JSON, not sure where JSON headers are coming from, though.., but it seems when you log req.body later, it's already processed by multer (you might move validator to separate middleware, so that it doesn't reach route handler/next middleware, see: Example: creating own validation runner).

    So, move multer middleware before express validator middleware, so that it recieves processed body object (fronted request looks fine, no need to set content type, it's being handled by itself, the boundary error is what you get when trying to set it manually):

    router.post(
      "/",
      verifyToken,
    
      //  multer needs to process request first to obtain req.body
      upload.array("imageFiles", 6),
    
      [
        body("name").notEmpty().withMessage("Name is required"),
        //...
      ],
      async (req: Request, res: Response) => {
        console.log("req body");
        console.log(req.body);