next.js file upload via api routes / formidable - not working
Asked Answered
R

4

16

I'm having a hell of a time getting file upload to work via api routes.

On client-side im submitting the file like so:

 onFormSubmit = (e) => {
    e.preventDefault() // Stop form submit

    this.fileUpload(this.state.file).then((response) => {
      console.log('rD', response.data)
    })
 }

 onFileChange = (e) => {
    this.setState({ file: e.target.files[0] })
 }

 fileUpload = (file) => {
    const url = '/api/mail/upload'
    const formData = new FormData()
    formData.append('file', file)
    const config = {
      headers: {
        'X-CSRF-TOKEN': this.props.session.csrfToken
      }
    }
    return axios.post(url, formData, config)
 }

My request to /api/mail/upload then looks like this:

Request Headers:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,de-DE;q=0.8,de;q=0.7
Connection: keep-alive
Content-Length: 1331
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBlNt6z8t4rGZT0x6
Cookie: abc123
Host: localhost:3000
Origin: http://localhost:3000
Referer: http://localhost:3000/new
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36
X-CSRF-TOKEN: abc123

Form Data:
file: (binary)

Then in the route (/api/mail/upload) I'm trying to use formidable to parse the form data and finally do something with the file.

I've made sure to disable the built-in body parser by including the following at the bottom of the api route file:

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

^^ Thats the correct way to do that, right?

Finally, in the api route, I've tried many different things, but currently the following is what I expect to work, but it is not..

module.exports = async (req, res) => {
  const form = new formidable.IncomingForm()

  form.parse(req, (err, fields, files) => {
    if (err) return reject(err)
    console.log(fields, files)
    res.status(200).json({ fields, files })
  })
  // if I console.log(form) here - I can see the request details, so it seems to be picking that up
}

This generates no output whatsoever on the server-side nor on client-side, i expect the console.log(fields, files) to output on the server-side the name of the file, etc.

Anyone know what I am missing?

Rakish answered 1/2, 2020 at 19:7 Comment(1)
In case anyone comes across this and has a similar issue - I didn't end up figuring this exact issue out, but I worked around it by using cloudinaries free service (ca. 25gb ~ month if you're just doing basic uploads of files without any transformations, etc.) using their example from on codepen (simple xhr upload): codepen.io/team/Cloudinary/pen/QgpyOKRakish
R
6

I am 100% sure your issue is caused by intermixing CommonJS exports (i.e. module.exports = ) and ES6 modules syntax (export const config = ...).

When you do this NextJS sees only default export and doesn't see your config declaration and hence continues to parse body. Which, in turn, results in req to be already completed by the time it's being passed to formidable.

Apart from that everything looks right in your code. By just changing

module.exports = async (req, res) => {

to

export default async (req, res) => {

you should get an expected behaviour.

Resolute answered 19/7, 2020 at 11:26 Comment(0)
J
26

All you need is disable Next.js built-in body parser: https://nextjs.org/docs/api-routes/api-middlewares#custom-config

Here are working example:

// Backend
import formidable from 'formidable';

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

export default async (req, res) => {
  const form = new formidable.IncomingForm();
  form.uploadDir = "./";
  form.keepExtensions = true;
  form.parse(req, (err, fields, files) => {
    console.log(err, fields, files);
  });
};

https://gist.github.com/agmm/da47a027f3d73870020a5102388dd820

Jug answered 3/12, 2020 at 9:8 Comment(2)
Yes, this works, indeed.Nitrogenize
and <form encType="multipart/form-data" method="post">Aerodrome
S
6

I believe that if you are doing asynchronous operations inside of the endpoint then next.js expects you to return a promise e.g

export default (req, res) => {
 const promise = new Promise((resolve, reject) => {

  const form = new formidable.IncomingForm();

  form.parse(req, (err, fields, files) => {
    if (err) reject(err);
    resolve({fields, files});
  })

 })

 return promise.then(({fields, files}) => {
    res.status(200).json({ fields, files })
 })
}
Sagacious answered 12/5, 2020 at 21:31 Comment(0)
R
6

I am 100% sure your issue is caused by intermixing CommonJS exports (i.e. module.exports = ) and ES6 modules syntax (export const config = ...).

When you do this NextJS sees only default export and doesn't see your config declaration and hence continues to parse body. Which, in turn, results in req to be already completed by the time it's being passed to formidable.

Apart from that everything looks right in your code. By just changing

module.exports = async (req, res) => {

to

export default async (req, res) => {

you should get an expected behaviour.

Resolute answered 19/7, 2020 at 11:26 Comment(0)
O
1

you can implement it in a simple way as explained below->

sending a file from client side in a simple way will be ->

client side->

const file = e.target.files[0];
const formData = new FormData();
formData.append("file", file);

const { data } = await axios.post("/api/upload",formData)

install express-formidable at backend

backend api will be ->

import express from 'express';
import expressAsyncHandler from 'express-async-handler';
import formidable from 'express-formidable';

const router = express.Router();
export const config = {
    api: {
       bodyParser: false,
    },
};
     
export default router.post('/upload',formidable() ,expressAsyncHandler(async (req, res) => {
    const {file} = req.files;
    console.log(file);
    // here you can handle the incoming file to send it to cloud
      
}));

express-formidable will handle all the incoming stream and provide us data by storing all data to 'data' variable. In order to store it to any cloud (aws s3, cloudinary) , create a readable stream and append it to form data and then send it .

Obscurant answered 19/10, 2021 at 7:28 Comment(1)
How to use bodyParser with ES6's module.exports ?Yardage

© 2022 - 2024 — McMap. All rights reserved.