firebasexmlhttprequestgoogle-cloud-functionsmultipartform-databusboy

How to parse multipart/form-data on firebase cloud functions?


I've been trying to post a multipart/form-data object with text and an image file to one of my cloud functions, according to the documents here:

https://cloud.google.com/functions/docs/writing/http#multipart_data_and_file_uploads

I've kept my cloud function almost entirely the same as the example, except I've wrapped it in a CORS response. It seems though, that no matter what, busboy's 'field' and 'file' events never fire, and when I print the request body's toString method, I get some of the data, before it goes to gibberish.

Is it possible I'm setting something up incorrectly when sending the FormData?

Here's the code containing my XMLHttpRequest():

var formData = new FormData(document.getElementById("ticketForm"));
return new Promise(function (resolve, reject) {
      var xmlhttp = new XMLHttpRequest();
      xmlhttp.open("POST", "https://us-central1-XXXXXXX.cloudfunctions.net/ticketFunction");
      var boundary = Math.random().toString().substr(8) + "--";
      xmlhttp.setRequestHeader('Content-Type', 'multipart/form-data;charset=utf-8; boundary=' + boundary);
      // xmlhttp.setRequestHeader('Content-Type', undefined);

      xmlhttp.onload = function () {
        if (this.status >= 200 && this.status < 300) {
          resolve(xmlhttp.response);
        } else {
          reject({
            status: this.status,
            statusText: xmlhttp.statusText
          });
        }
      };
      xmlhttp.onerror = function () {
        reject({
          status: this.status,
          statusText: xmlhttp.statusText
        });
      };
      xmlhttp.send(formData);
    });

Here's my cloud function:

exports.newTicketWithPhoto = functions.https.onRequest((req, res) => { cors(req, res, () => {

if (req.method === 'POST') {

  const busboy = new Busboy({ headers: req.headers });
  const tmpdir = os.tmpdir();
  console.log("Length: " + req.headers['content-length']);
  console.log(req.body.toString());

  // This object will accumulate all the fields, keyed by their name
  const fields = {};

  // This object will accumulate all the uploaded files, keyed by their name.
  const uploads = {};

  // This code will process each non-file field in the form.
  busboy.on('field', (fieldname, val) => {
    // TODO(developer): Process submitted field values here
    console.log(`Processed field ${fieldname}: ${val}.`);
    fields[fieldname] = val;
  });

  busboy.on('error', function(err){
    console.log("Error: " + err);
  });

  // This code will process each file uploaded.
  busboy.on('file', (fieldname, file, filename) => {
    // Note: os.tmpdir() points to an in-memory file system on GCF
    // Thus, any files in it must fit in the instance's memory.
    console.log(`Processed file ${filename}`);
    const filepath = path.join(tmpdir, filename);
    uploads[fieldname] = filepath;
    file.pipe(fs.createWriteStream(filepath));
  });

  // This event will be triggered after all uploaded files are saved.
  busboy.on('finish', () => {
    // TODO(developer): Process uploaded files here
    console.log(fields);
    console.log("Uploads: " + JSON.stringify(uploads));
    for (const name in uploads) {
      console.log(name);
      const file = uploads[name];
      fs.unlinkSync(file);
    }
    res.send();
  });

  req.pipe(busboy);
} else {
  // Return a "method not allowed" error
  res.status(405).send("Something weird happened");
}

}) });

A couple of things I notice is this: Printing the content-length value of the header always seems to return undefined.

When I print the req.body.toString() method, I get this:

 ------WebKitFormBoundaryeYZHuHsOLlohyekc
Content-Disposition: form-data; name="description"

testing description
------WebKitFormBoundaryeYZHuHsOLlohyekc
Content-Disposition: form-data; name="priority"

Low
------WebKitFormBoundaryeYZHuHsOLlohyekc
Content-Disposition: form-data; name="dueDate"

2018-07-27
------WebKitFormBoundaryeYZHuHsOLlohyekc
Content-Disposition: form-data; name="customer"

zavtra
------WebKitFormBoundaryeYZHuHsOLlohyekc
Content-Disposition: form-data; name="email"

test@test.com
------WebKitFormBoundarysseArmLvKhJY0TAm
Content-Disposition: form-data; name="photo"; filename="brighthabits1.png"
Content-Type: image/png

�PNG

IHRGB���@IDATx�}�ݴտ��I�$�V���*�EH ! �:(_7m)-ݻ�ί���{-dCaf��*�=!����N����ͽ�ږm�y�׶tt�OG�ʶ,6L���L*�ć[����V;�x�+[�c�/�0;@a�5��;��]]<x��\R�cqoG`rGƵ�t����O�y�J���"
����*�,�F,��.�ib�
                 ��I�.�SV�;��h�!v��~T�EY����(u\�4+&��I��9@~wP�`N��H�;�G"7.BI��h
                                                                               P��$R
                                                                                    �0pt,�[=��E��8����$^$��
"�,�,�4�>�Y�YY|�v3JSW��
                       )�q,���i>w��A��q\-
                                         �u���ՠ�hJW�oF������W7X��]��
                                                                    )#mx������&�њ�����iu���;D��ŗL��ޥh[F�8���D�^������IW��#��

                                �
                                 �
�TL�n���� {�l�`h����r   ��S>�[���&���_�%R8���W��mok�E����R���.]#@5������j���o���e����?Ӟ�u�Ţ�Y��5�N'�Nf��Թ#ߏ��E;�<�?^X��x�uπʭ�V??�� s�plzBǶ 

I'm not sure what causes all the gibberish at the end, but that's explicitly only when I upload an image. When there's no image in the form data, busboy's 'field' events still don't fire, leading me to believe something still isn't being parsed correctly.

It's frustrating because it otherwise seems like I'm following the documentation exactly correctly.


Solution

  • // Node.js doesn't have a built-in multipart/form-data parsing library.
    // Instead, we can use the 'busboy' library from NPM to parse these requests.
    const Busboy = require("busboy")
    const busboy = new Busboy({ headers: request.headers })
    let fields = []
    busboy.on("field", (field, val) => {
        console.log(`Processed field ${field}: ${val}.`)
        fields[field] = val
    })
    busboy.end(request.rawBody)