Search code examples
node.jsexpressmongoosemulterexpress-validator

Express-validator not executing after multer function


I have a form in my express application for users to enter some text, and also upload 1-3 photos. I'm handling the file uploads to my S3 bucket with multer, and I'm validating the rest of the form with express-validator.

This makes my POST route look like:

router.post("/list-product", listLimiter, function (req, res) {
  singleUpload(req, res, function (err) {
    // if any multer errors, redirect to form
    if (err) {
      res.redirect(
        "list-product" +
          "?error=Image error, please make sure your file is JPG or PNG"
      );
      return;
    }
    // if no multer errors, validate rest of form
  });
});

I'm having issues integrating express-validator. I've been stuck on this problem for a few days now, and I think I'm getting close. My code below will catch the multer error, and it will create a new Product if all the inputs are filled out. So what's happening is my express-validator isn't catching the errors here if (!errors.isEmpty()) { // handle errors and its just skipping it and going straight to else { let product = new Product({. I know this because when I leave the inputs empty, it throws a mongoose error of missing schema paths.

let upload = require("../controllers/uploadController");
const singleUpload = upload.single("image");


router.post("/list-product", listLimiter, function (req, res, next) {
  singleUpload(req, res, function (err) {
    // if any multer errors, redirect to form
    if (err) {
      res.redirect(
        "list-product" +
          "?error=Image error, please make sure your file is JPG or PNG"
      );
      return;
    }
    // if no multer errors, validate rest of form

    body("productName")
      .trim()
      .isLength({ min: 1 })
      .withMessage("Please enter the name of your product"),
      body("productPrice")
        .isNumeric()
        .withMessage("Please enter a valid price"),
      body("productCategory")
        .trim()
        .isLength({ min: 1 })
        .withMessage("Please select the category of your product"),
      body("productDescription")
        .trim()
        .isLength({ min: 50 })
        .withMessage("Minimum 50 characters")
        .isLength({ max: 500 })
        .withMessage("Maximum 500 characters"),
      body("businessName")
        .trim()
        .isLength({ min: 1 })
        .withMessage("Please enter the name of your business"),
      body("website")
        .trim()
        .isURL()
        .withMessage("Please enter the URL for your product or business");

    check("*").escape();

    const errors = validationResult(req);
    let errArray = errors.array();

    if (!errors.isEmpty()) {
      res.render("list", {
        form: req.body,
        errors: errArray,
        msg: "Please check the form for errors",
        option: req.body.productCategory,
      });
      return;
    } else {
      let product = new Product({
        business: req.body.businessName,
        productName: req.body.productName,
        category: req.body.productCategory,
        price: req.body.productPrice,
        description: req.body.productDescription,
        website: req.body.website,
        imageURL:
          "https://mybucket.s3-us-west-2.amazonaws.com/" + req.imageName,
        imageURL2:
          "https://mybucket.s3-us-west-2.amazonaws.com/" + req.imageName,
      });
      product.save(function (err) {
        if (err) {
          console.log(err);
          return next(err);
        }
        res.redirect("/list-product");
      });
    }
  });
});

I would truly appreciate any help or advice since I have been stuck on this for a few days and am feeling really stupid!..

I will include one final block of code, this is my express-validator function that works when I'm just validating the text inputs, so I know this approach does work.. I'm just having a real tough time combining it with the multer function

exports.list__post = [
  body("productName")
    .trim()
    .isLength({ min: 1 })
    .withMessage("Please enter the name of your product"),
  body("productPrice")
    .isNumeric()
    .withMessage("Please enter a valid price"),
  body("productCategory")
    .trim()
    .isLength({ min: 1 })
    .withMessage("Please select the category of your product"),
  body("productDescription")
    .trim()
    .isLength({ min: 50 })
    .withMessage("Minimum 50 characters")
    .isLength({ max: 500 })
    .withMessage("Maximum 500 characters"),
  body("businessName")
    .trim()
    .isLength({ min: 1 })
    .withMessage("Please enter the name of your business"),
  body("website")
    .trim()
    .isURL()
    .withMessage("Please enter the URL for your product or business"),

  check("*").escape(),

  (req, res, next) => {
    const errors = validationResult(req);
    let errArray = errors.array();

    if (!errors.isEmpty()) {
      console.log(req.body)
      res.render("list", {
        form: req.body,
        errors: errArray,
        msg: "Please check the form for errors",
        option: req.body.productCategory,
      });
      return;
    } else {
      
      let product = new Product({
        business: req.body.businessName,
        productName: req.body.productName,
        category: req.body.productCategory,
        price: req.body.productPrice,
        description: req.body.productDescription,
        website: req.body.website,
        imageURL: "https://mybucket.s3-us-west-2.amazonaws.com/" + req.imageName,
        imageURL2:"https://mybucket.s3-us-west-2.amazonaws.com/" + req.imageName,
      });
      product.save(function (err) {
        if (err) {
          console.log(err);
          return next(err);
        }
        res.redirect("/list-product");
      });
    }
  },
];

Update:

// uploadController.js

const aws = require("aws-sdk");
const multer = require("multer");
const multerS3 = require("multer-s3");
const crypto = require("crypto");
require("dotenv").config();

// config aws
aws.config.update({
  secretAccessKey: process.env.SECRETACCESSKEY,
  accessKeyId: process.env.ACCESSKEYID,
  region: "us-west-2",
});
const s3 = new aws.S3();

const fileFilter = (req, file, cb) => {
  if (file.mimetype === "image/jpeg" || file.mimetype === "image/png") {
    cb(null, true);
  } else {
    cb(new Error("Invalid format, only JPG and PNG"), false);
  }
};

const upload = multer({
  fileFilter: fileFilter,
  limits: { fileSize: 1024 * 1024 },
  storage: multerS3({
    s3: s3,
    bucket: "mybucket",
    acl: "public-read",
    metadata: function (req, file, cb) {
      cb(null, { fieldName: file.fieldname });
    },
    key: function (req, file, cb) {
      req.imageName = crypto.randomBytes(16).toString("hex");
      cb(null, req.imageName);
    },
  }),
});

module.exports = upload;


Solution

  • Quick Fixes:

    • call singleUpload in middleware, and here you are uploading single file using upload.single(), so you need to remove multiple attribute from view file tag, if you want to upload multiple file then use upload.array('field name', total count of files)
    router.post("/list-product", 
      singleUpload, 
    
    • validate other parameters in middleware
    • I have noticed you haven't added businessName field in form, so this will return with error, you can add it in form or remove validation from here, and also in schema.
      [
        body("productName").trim().isLength({ min: 1 }).withMessage("Please enter the name of your product"),
        body("productPrice").isNumeric().withMessage("Please enter a valid price"),
        body("productCategory").trim().isLength({ min: 1 }).withMessage("Please select the category of your product"),
        body("productDescription").trim().isLength({ min: 50 }).withMessage("Minimum 50 characters").isLength({ max: 500 }).withMessage("Maximum 500 characters"),
        body("businessName").trim().isLength({ min: 1 }).withMessage("Please enter the name of your business"),
        body("website").trim().isURL().withMessage("Please enter the URL for your product or business"),
        check("*").escape()
      ], 
    
    • middleware callback function, here req will provide all body parameters and file object that is uploaded,
    • just console req.body and using req.file will return all details of file that is uploaded in S3, including filename and location, i suggest you to use file name and location from this object, I have console it already,
    • handle file extension error using req.fileTypeInvalid, that we have passed from fileFitler
      function (req, res, next) {
        console.log(req.body, req.file);
    
        // FILE EXTENSION ERROR
        if (req.fileTypeInvalid) {
            res.redirect("list-product" + "?error=" + req.fileTypeInvalid);
            return;
        }
    
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            let errArray = errors.array();
            let errorsObj = {}; // access errors indiviually 
            errArray.map((item) => {
                const id = item.param;
                delete item.param;
                errorsObj[id] = item;
            });
            res.render("list", {
                form: req.body,
                errors: errorsObj,
                msg: "Please check the form for errors",
                option: req.body.productCategory,
            });
            return;
        }
    
        let product = new Product({
            business: req.body.businessName,
            productName: req.body.productName,
            category: req.body.productCategory,
            price: req.body.productPrice,
            description: req.body.productDescription,
            website: req.body.website,
            imageURL: "https://mybucket.s3-us-west-2.amazonaws.com/" + req.imageName,
        });
        product.save(function (err) {
            if (err) {
                console.log(err);
                return next(err);
            }
            res.redirect("/list-product");
        });
    });
    

    Combined Final Version of Request:

    router.post("/list-product", 
      singleUpload, 
      [
        body("productName").trim().isLength({ min: 1 }).withMessage("Please enter the name of your product"),
        body("productPrice").isNumeric().withMessage("Please enter a valid price"),
        body("productCategory").trim().isLength({ min: 1 }).withMessage("Please select the category of your product"),
        body("productDescription").trim().isLength({ min: 50 }).withMessage("Minimum 50 characters").isLength({ max: 500 }).withMessage("Maximum 500 characters"),
        body("businessName").trim().isLength({ min: 1 }).withMessage("Please enter the name of your business"),
        body("website").trim().isURL().withMessage("Please enter the URL for your product or business"),
        check("*").escape()
      ], 
      function (req, res, next) {
        console.log(req.body, req.file);
    
        // FILE EXTENSION ERROR
        if (req.fileTypeInvalid) {
            res.redirect("list-product" + "?error=Image error, please make sure your file is JPG or PNG");
            return;
        }
    
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            let errArray = errors.array();
            let errorsObj = {}; // access errors indiviually 
            errArray.map((item) => {
                const id = item.param;
                delete item.param;
                errorsObj[id] = item;
            });
            res.render("list", {
                form: req.body,
                errors: errorsObj,
                msg: "Please check the form for errors",
                option: req.body.productCategory,
            });
            return;
        }
    
        let product = new Product({
            business: req.body.businessName,
            productName: req.body.productName,
            category: req.body.productCategory,
            price: req.body.productPrice,
            description: req.body.productDescription,
            website: req.body.website,
            imageURL: "https://mybucket.s3-us-west-2.amazonaws.com/" + req.imageName,
        });
        product.save(function (err) {
            if (err) {
                console.log(err);
                return next(err);
            }
            res.redirect("/list-product");
        });
    });


    One more correction in fileFilter inside uloadController.js file, pass error in req.fileTypeInvalid and return it, it is handled in request,

    const fileFilter = (req, file, cb) => {
        if (file.mimetype === "image/jpeg" || file.mimetype === "image/png") {
            cb(null, true);
        } else {
            req.fileTypeInvalid = "Invalid format, only JPG and PNG";
            cb(null, false, req.fileTypeInvalid);
        }
    };