Home > front end >  React NextJS unable to upload a file through nextjs api at the same time as my form data 501 Not Imp
React NextJS unable to upload a file through nextjs api at the same time as my form data 501 Not Imp

Time:09-06

I have a nextjs app with a form in which I give the user the option to upload a file at the same time as filling out their personal data. I am using Multer and in the next-connect middleware I am using, it has me set bodyParser to false. As I am collecting other data, I get an error 501 cannot implement saying "error": "Sorry something Happened! Cannot destructure property 'firstName' of 'req.body' as it is undefined."

I then resorted to another piece of middleware called multiparty which I am using in the addcv route of the nextjs api. I am still getting the same error. If anyone can help me sort out this issue, I would appreciate it. I don't understand why it is not possible to upload a file at the same time as the form data as I could do this in a normal nodejs app without issue.

Here are the relevant parts of the Form:

CVForm.jsx
import { useSubmitCV } from '../hooks/useSubmitCV';
function CVForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    cvfile: '',
    address: '',
    usEligible: '',
    city: '',
    state: '',
    zip: '',
    dob: '',
    description: '',
  });
  const [blockOneOpen, setBlockOneOpen] = useState(false);
  const [havecv, setHaveCv] = useState(false);
  const [haveNoCv, setHaveNoCv] = useState(false);
  const [buttonClicked, setButtonClicked] = useState(false);
  const [buttonText, setButtonText] = useState('Click Here To Start');

  const { submitcv, error, loading } = useSubmitCV();

  const onChange = (e) => {
    setFormData((prevState) => ({
      ...prevState,
      [e.target.name]: e.target.value,
    }));
  };

  const onFileChange = (e) => {
    if (!cvfile) {
      setFormData((prevState) => ({
        ...prevState,
        cvfile: null,
      }));
    } else {
      setFormData((prevState) => ({
        ...prevState,
        cvfile: e.target.files[0],
      }));
    }
  };
const cvSubmit = async (e) => {
    e.preventDefault();
    if (
      formData.firstName === '' ||
      formData.lastName === '' ||
      formData.email === '' ||
      formData.phone === ''
    ) {
      toast.error('All Fields are required');
    } else {
      await submitcv(formData);
    }
    console.log(formData);
    setButtonClicked(!buttonClicked);
    setButtonText('Start Here');
  };
// JSX PORTION 
<label className='block mb-6 mt-6 text-sm font-medium text-gray-900 dark:text-gray-300'                       
  htmlFor='file_input'>Upload Resume
</label>
<input
className='block w-full text-sm text-white bg-gray-50 rounded-lg border border-gray-300 cursor-pointer dark:text-white focus:outline-none dark:bg-[#00388d] dark:border-gray-600 dark:placeholder-white'
id='cvfile'
type='file'
filename='cvfile'
name='cvfile'
onChange={onFileChange}
accept='.doc,.docx,.pdf,.odf' />
   <div className='mb-6 mt-6'>
      <button type='submit'className='w-full px-2 py-4 text-white bg-[#00388d] rounded-md  focus:bg-indigo-600 focus:outline-none'>
      Send Resume
     </button>
</div>

The Use Submit Hook useSubmitCV.js

import { useState } from 'react';
import { toast } from 'react-toastify';
import axios from 'axios';
import { useRouter } from 'next/router';

export const useSubmitCV = () => {
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(null);

  const router = useRouter();

  // const { dispatch } = useCVContext();

  const API_URL = '/api/cvs/addcv';

  const submitcv = async (formData) => {
    setLoading(true);
    setError(null);
    const form = new FormData();
    form.append('firstName', formData.firstName);
    form.append('lastName', formData.lastName);
    form.append('email', formData.email);
    form.append('phone', formData.phone);
    form.append('cvfile', formData.cvfile);
    form.append('fileUrl', formData.fileUrl);
    form.append('address', formData.address);
    form.append('city', formData.city);
    form.append('state', formData.state);
    form.append('zip', formData.zip);
    form.append('usEligible', formData.usEligible);
    form.append('dob', formData.dob);
    form.append('description', formData.description);

    const response = await axios.post(API_URL, form);
    if (response.data.message) {
      setError(response.data.message);
      toast.error(response.data.message);
      setLoading(false);
      return;
    }
    toast.success('Resume Successfully Submitted');
    setLoading(false);
    router.push('/');
  };
  return { submitcv, loading, error };
};

The Middleware file middleware.js

import nextConnect from 'next-connect';
import multiparty from 'multiparty';

const middleware = nextConnect();

middleware.use(async (req, res, next) => {
  const form = new multiparty.Form();

  await form.parse(req, function (err, fields, files) {
    req.body = fields;
    req.files = files;
    next();
  });
});

export default middleware;

And Finally the addcv.js Route within the api route of nextjs

import nextConnect from 'next-connect';
import multer from 'multer';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs';
import path from 'path';
import middleware from '../../../middleware/middleware';

import { File, Web3Storage } from 'web3.storage';

import CV from '../../../models/cvModel';

const w3storage = new Web3Storage({ token: process.env.WEB3_STORAGE_TOKEN });

function checkFileType(file, cb) {
  // Allowed ext
  const filetypes = /doc|docx|pdf|odf/;
  // Check ext
  const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
  // Check mime
  const mimetype = filetypes.test(file.mimetype);

  if (mimetype && extname) {
    return cb(null, true);
  } else {
    cb('Error: Documents Only!');
  }
}

const storage = multer.memoryStorage({
  destination: (req, file, cb) => {
    cb(null, path.join(process.cwd(), 'public', 'uploads'));
  },
  filename: (req, file, cb) => {
    cb(null, uuidv4()   '-'   file.originalname);
  },
  fileFilter: function (_req, file, cb) {
    checkFileType(file, cb);
  },
});

const maxSize = 3 * 1000 * 1000;

const upload = multer({ storage: storage, limits: { fileSize: maxSize } });

const createCV = nextConnect({
  one rror(error, req, res) {
    res
      .status(501)
      .json({ error: `Sorry something Happened! ${error.message}` });
  },
  onNoMatch(req, res) {
    res.status(405).json({ error: `Method '${req.method}' Not Allowed` });
  },
})
  .use(middleware)
  .use(upload.single('cvfile'))
  .post((req, res) => {
    // @desc Create a new cv
    // @route POST /api/cvs/addcv
    // @access Public

    const {
      firstName,
      lastName,
      email,
      phone,
      cvfile,
      address,
      city,
      state,
      zip,
      usEligible,
      dob,
      description,
    } = req.body;

    const fileUrl = req.file.filename;

    console.log(fileUrl);
    const bytes = fs.readFileSync(`${process.env.UPLOAD_PATH}/${fileUrl}`);
    const newFile = new File([bytes], fileUrl);
    const cid = w3storage.put([newFile]);
    const filePath = path.join(process.cwd(), 'public', 'uploads', fileName);
    if (fs.existsSync(filePath)) {
      fs.unlinkSync(filePath);
    }
    const cv = CV.create({
      firstName,
      lastName,
      email,
      phone,
      cvfile,
      address,
      city,
      state,
      zip,
      usEligible,
      dob,
      description,
      fileUrl: `https://${cid}.ipfs.dweb.link/${fileName}`,
    });

    res.status(201).json(cv);
    console.log(req.body);
    // res.status(201).json({ body: req.body, file: req.file });
  });

export default createCV;

export const config = {
  api: {
    bodyParser: false, // Disallow body parsing, consume as stream
  },
};

Again, if anyone knows how to do this without having to go outside of nextjs api routing system, I would sure appreciate it. Thank you in advance.

EDIT:: I ran a debugger and got the following from the form values after submit in the useSubmit.js enter image description here

This shows the value of cvfile as cv.docx which is correct but for the value of fileUrl (which is kind of important :) ) is set to undefined. Not good. And worse yet, because I am new to Next and a little green in this area, I don't know how to fix it.

EDIT 2:: On Debugger; my code dies after this block of code:

const response = await axios.post(API_URL, form, {
    headers: {
      'Content-Type': 'multipart/formdata',
    },
  });

CodePudding user response:

If you read multiparty's README, I quote:

Parse http requests with content-type multipart/form-data, also known as file uploads.

(I love the creative name of the package.) Your form needs to send data that's of multipart/form-data (you're using application/x-www-form-urlencoded (default value)).

In useSubmitCV.js alter the axios request to include the custom content type:

// ...
await axios.post(API_URL, form, {
  headers: {
    'Content-Type': 'multipart/formdata'
  }
});

This should allow multiparty to receive your data.

CodePudding user response:

In the end, I had to simplify the multer and add file functionality. I removed the multiparty middleware and just stuck with nextConnect. Here is the revised code:

import nextConnect from 'next-connect';
import multer from 'multer';
import path from 'path';
import connectDB from '../../../config/db';

import { File, Web3Storage } from 'web3.storage';

import CV from '../../../models/cvModel';

export const config = {
  api: {
    bodyParser: false,
  },
};

const w3storage = new Web3Storage({ token: process.env.WEB3_STORAGE_TOKEN });

function checkFileType(file, cb) {
  // Allowed ext
  const filetypes = /doc|docx|pdf|odf/;
  // Check ext
  const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
  // Check mime
  const mimetype = filetypes.test(file.mimetype);

  if (mimetype && extname) {
    return cb(null, true);
  } else {
    cb('Error: Documents Only!');
  }
}

const storage = multer.memoryStorage({
  fileFilter: function (_req, file, cb) {
    checkFileType(file, cb);
  },
});

const maxSize = 3 * 1000 * 1000;

const upload = multer({ storage: storage, limits: { fileSize: maxSize } });

const createCV = nextConnect({
  one rror(error, req, res) {
    res
      .status(501)
      .json({ error: `Sorry something Happened! ${error.message}` });
  },
  onNoMatch(req, res) {
    res.status(405).json({ error: `Method '${req.method}' Not Allowed` });
  },
})
  .use(upload.single('cvfile'))
  .post(async (req, res) => {
    // @desc Create a new cv
    // @route POST /api/cvs/addcv
    // @access Public
    await connectDB();

    try {
      const {
        firstName,
        lastName,
        email,
        phone,
        address,
        city,
        state,
        zip,
        usEligible,
        dob,
        description,
      } = req.body;

      console.log(req.body, req.file);

      const newFile = new File([req.file.buffer], req.file.originalname);
      const cid = await w3storage.put([newFile]);

      const cv = await CV.create({
        firstName,
        lastName,
        email,
        phone,
        address,
        city,
        state,
        zip,
        usEligible,
        dob,
        description,
        fileUrl: `https://${cid}.ipfs.dweb.link/${req.file.originalname}`,
      });

      return res.status(201).json(cv);

      // res.status(201).json({ body: req.body, file: req.file });
    } catch (error) {
      console.log(error);
      return res.status(400).json({ error: error.message });
    }
  });

export default createCV;

As you can see, since I am storing the files in web3 storage, there is no longer any need to use diskStorage so a lot of that code has been stripped out. I also removed the cvfile variable from this code as well as the model as it was not needed. I hope this helps anyone else having this particular issue.

  • Related