Uploading a file with FormData and multer

23,553

Solution 1

multer uses multipart/form-data content-type requests for uploading files. Removing this bit from your doRequestSetHeaders function should fix your problem:

if(method === "POST" || method === "PUT"){
   r.setRequestHeader("Content-Type", "application/json");
}

You don't need to specify the content-type since FormData objects already use the right encoding type. From the docs:

The transmitted data is in the same format that the form's submit() method would use to send the data if the form's encoding type were set to multipart/form-data.

Here's a working example. It assumes there's a dropzone with the id drop-zone and an upload button with an id of upload-button:

var dropArea  = document.getElementById("drop-zone");
var uploadBtn = document.getElementById("upload-button");
var files     = [];

uploadBtn.disabled = true;
uploadBtn.addEventListener("click", onUploadClick, false);

dropArea.addEventListener("dragenter", prevent, false);
dropArea.addEventListener("dragover",  prevent, false);
dropArea.addEventListener("drop", onFilesDropped, false);   

//----------------------------------------------------
function prevent(e){

    e.stopPropagation();
    e.preventDefault();
}

//----------------------------------------------------
function onFilesDropped(e){

    prevent(e);

    files = e.dataTransfer.files;

    if (files.length){
        uploadBtn.disabled = false;
    }
}

//----------------------------------------------------
function onUploadClick(e){

    if (files.length){
        sendFile(files[0]);
    }
}

//----------------------------------------------------
function sendFile(file){

    var formData = new FormData();
    var xhr      = new XMLHttpRequest();

    formData.append("track", file, file.name);

    xhr.open("POST", "http://localhost:3000/tracks/upload", true);

    xhr.onreadystatechange = function () {  
        if (xhr.readyState === 4) {  
            if (xhr.status === 200) {  
                console.log(xhr.responseText);
            } else {  
                console.error(xhr.statusText);  
            }  
        }  
    };

    xhr.send(formData);
}

The server side code is a simple express app with the exact router code you provided.

Solution 2

to post a FormData object accepted by multer the upload function should be like this:

function uploadFile(fileToUpload, url) { 
    var formData = new FormData();
    //append file here 
    formData.append('file', fileToUpload, fileToUpload.name);
    //and append the other fields as an object here
    /* var user = {name: 'name from the form',
        email: 'email from the form' 
        etc...       
    }*/
    formData.append('user', user);

    // This function simply creates an XMLHttpRequest object
    // Opens the connection and sends form_data
    doJSONRequest("POST", "/tracks/upload", null, formData, function(d) {
        console.log(d);
    })
}
Share:
23,553
nbro
Author by

nbro

don't believe the hype

Updated on July 01, 2020

Comments

  • nbro
    nbro almost 4 years

    I have successfully managed to upload a file to a Node server using the multer module by selecting the file using the input file dialog and then by submitting the form, but now I would need, instead of submitting the form, to create a FormData object, and send the file using XMLHttpRequest, but it isn't working, the file is always undefined at the server-side (router).

    The function that does the AJAX request is:

    function uploadFile(fileToUpload, url) {
    
      var form_data = new FormData();
    
      form_data.append('track', fileToUpload, fileToUpload.name);
    
      // This function simply creates an XMLHttpRequest object
      // Opens the connection and sends form_data
      doJSONRequest("POST", "/tracks/upload", null, form_data, function(d) {
        console.log(d);
      })
    
    }
    

    Note that fileToUpload is defined and the url is correct, since the correct router method is called. fileToUpload is a File object obtained by dropping a file from the filesystem to a dropzone, and then by accessing the dataTransfer property of the drop event.

    doJSONRequest is a function that creates a XMLHttpRequest object and sends the file, etc (as explained in the comments).

    function doJSONRequest(method, url, headers, data, callback){
    
      //all the arguments are mandatory
      if(arguments.length != 5) {
        throw new Error('Illegal argument count');
      }
    
      doRequestChecks(method, true, data);
    
      //create an ajax request
      var r = new XMLHttpRequest();
    
      //open a connection to the server using method on the url API
      r.open(method, url, true);
    
      //set the headers
      doRequestSetHeaders(r, method, headers);
    
      //wait for the response from the server
      r.onreadystatechange = function () {
        //correctly handle the errors based on the HTTP status returned by the called API
        if (r.readyState != 4 || (r.status != 200 && r.status != 201 && r.status != 204)){
          return;
        } else {
          if(isJSON(r.responseText))
            callback(JSON.parse(r.responseText));
          else if (callback !== null)
            callback();
        }
      };
    
      //set the data
      var dataToSend = null;
      if (!("undefined" == typeof data) 
        && !(data === null))
        dataToSend = JSON.stringify(data);
    
      //console.log(dataToSend)
    
      //send the request to the server
      r.send(dataToSend);
    }
    

    And here's doRequestSetHeaders:

    function doRequestSetHeaders(r, method, headers){
    
      //set the default JSON header according to the method parameter
      r.setRequestHeader("Accept", "application/json");
    
      if(method === "POST" || method === "PUT"){
        r.setRequestHeader("Content-Type", "application/json");
      }
    
      //set the additional headers
      if (!("undefined" == typeof headers) 
        && !(headers === null)){
    
        for(header in headers){
          //console.log("Set: " + header + ': '+ headers[header]);
          r.setRequestHeader(header, headers[header]);
        }
    
      }
    }
    

    and my router to upload files is the as follows

    // Code to manage upload of tracks
    var multer = require('multer');
    var uploadFolder = path.resolve(__dirname, "../../public/tracks_folder");
    
    function validTrackFormat(trackMimeType) {
      // we could possibly accept other mimetypes...
      var mimetypes = ["audio/mp3"];
      return mimetypes.indexOf(trackMimeType) > -1;
    }
    
    function trackFileFilter(req, file, cb) {
      cb(null, validTrackFormat(file.mimetype));
    }
    
    var trackStorage = multer.diskStorage({
      // used to determine within which folder the uploaded files should be stored.
      destination: function(req, file, callback) {
    
        callback(null, uploadFolder);
      },
    
      filename: function(req, file, callback) {
        // req.body.name should contain the name of track
        callback(null, file.originalname);
      }
    });
    
    var upload = multer({
      storage: trackStorage,
      fileFilter: trackFileFilter
    });
    
    
    router.post('/upload', upload.single("track"), function(req, res) {
      console.log("Uploaded file: ", req.file); // Now it gives me undefined using Ajax!
      res.redirect("/"); // or /#trackuploader
    });
    

    My guess is that multer is not understanding that fileToUpload is a file with name track (isn't it?), i.e. the middleware upload.single("track") is not working/parsing properly or nothing, or maybe it simply does not work with FormData, in that case it would be a mess. What would be the alternatives by keeping using multer?

    How can I upload a file using AJAX and multer?

    Don't hesitate to ask if you need more details.

  • nbro
    nbro over 8 years
    Did I say that my form is multipart, i.e. I have other type of fields in my form..?
  • cviejo
    cviejo over 8 years
    I'm not sure sure i understand your question? "content-type" for that request is set to "'multipart/form-data; boundary=----WebKitFormBoundary...", as expected. Otherwise multer wouldn't even work with it in the first place. You can add further fields using formData.append and read them from req.body.
  • nbro
    nbro over 8 years
    I haven't tried your code yet, but it really seems similar to mine, so I don't see why yours should work... could you please explain why should that work?
  • cviejo
    cviejo over 8 years
    To answer that question you should either post your complete client side code (not just uploadFile) or try mine. Mine does, again, work. Tested with your exact router code.
  • nbro
    nbro over 8 years
    I will post my doJSONRequest method implementation...See edit now.
  • cviejo
    cviejo over 8 years
    Are you setting content-type manually in doRequestSetHeaders? That'd be one difference to my code and why mine works and yours doesn't.
  • nbro
    nbro over 8 years
    Yes, I will also post the implementation of that function, if you want..see edit.
  • cviejo
    cviejo over 8 years
    Well, that is the reason why your code isn't working and mine isn't. Let's move this to a chat, though.
  • cviejo
    cviejo over 8 years