question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

parseMultipartData don't work with File class in Node.js

See original GitHub issue

Environment

Name Version
msw 0.35.0
node 16.11.1
OS OSX 10.15.3

Request handlers

// Example of declaration. Provide your code here.
import { setupServer } from 'msw/node'
import { rest } from 'msw'

const server = setupServer(
  rest.post(
    'https://api-data.line.me/v2/bot/audienceGroup/upload/byFile',
    (req, res, ctx) => {
      console.log(req);
      return res(
        ctx.json({
          audienceGroupId: 1234567890123,
          createRoute: 'MESSAGING_API',
          type: 'UPLOAD',
          description: 'audienceGroupName_01',
          created: 1613698278,
          permission: 'READ_WRITE',
          expireTimestamp: 1629250278,
          isIfaAudience: false,
        })
      );
    }
  ),
)

server.listen()

Actual request

import fs from 'fs';
import FormData from 'form-data';

const file = fs.createReadStream(
   path.resolve(`${__dirname}/fixtures/audiences.txt`)
 );

const form = new FormData();
form.append('description', 'description');
form.append('file', file, {
  contentType: 'text/plain',
 });

await axios.post('https://api-data.line.me/v2/bot/audienceGroup/upload/byFile', form, {
   headers: form.getHeaders(),
 });

Current behavior

It throws ReferenceError: File is not defined in this line: https://github.com/mswjs/msw/blob/321d8c1c1a17d5bab7b976c6bb0d195698230be6/src/utils/internal/parseMultipartData.ts#L87

Expected behavior

I expect there is a File class polyfill or something that works in Node.js

Screenshots

The error catched in try/catch block:

Issue Analytics

  • State:open
  • Created 2 years ago
  • Comments:7 (5 by maintainers)

github_iconTop GitHub Comments

2reactions
MartinJaskullacommented, May 2, 2022

In a way @chentsulin’s issue is not limited to just FormData or File.

Similar issues might occur for other body types:

type BodyInit = Blob | BufferSource | FormData | URLSearchParams | ReadableStream<Uint8Array> | string;

There are three challenges

  1. Service workers only transmit text, so msw needs to do custom parsing so that the user receives something usable as the request argument in their RequestHandler. In some cases the browser even modifies the request made by the user’s code. For example when the user makes a POST request with FormData as the body, the browser will add the Content-Type header automatically. msw then needs to parse the body back into an object.
  2. msw should behave similar in Node and the browser
  3. Node is missing is browser interfaces such as File, FormData etc.

Instead of having msw doing custom parsing or polyfilling, it could make sense to pass the original values the user has passed to fetch/axios back to the user’s RequestHandler.

For example in the issue description @chentsulin is already using the form-data library. We could save the original body (form) and the original headers from this request (by overwriting fetch)…

await axios.post('https://api-data.line.me/v2/bot/audienceGroup/upload/byFile', form, {
   headers: form.getHeaders(),
 });

…and reuse them when creating the request which is passed to RequestHandler

// parseWorkerRequest
const request: RestRequest = {
// parseIsomorphicRequest
const mockedRequest: MockedRequest = {

This means msw could remove some of its custom parsing logic and does not need any polyfills in Node, users can use whatever polyfill they want e.g.form-data and use the objects created by their polyfill library in their RequestHandler.

I have written in more detail about this here: https://github.com/mswjs/msw/pull/1188#discussion_r845824936

As I have written in the linked comment I don’t really know what I am talking about here 😄

Usage example

Without headers

await axios.post('url', form);

Browser

rest.post("/post", async (req, res, ctx) => {
  // req.body is the original body (native browser FormData). The modified body (with boundary value) returned from the service worker is ignored
  // req.headers has the Content-Type header which was added by the browser (The header includes a now useless boundary value. Useless because msw is not using it to parse the modified body)
})

Node

rest.post("/post", async (req, res, ctx) => {
  // req.body is the original body (whatever FormData polyfill the user installed)
  // req.headers is empty as Node does not add headers automatically like the browser does (?)
})

With headers

await axios.post('url', form, {headers: form.getHeaders()});

Browser

rest.post("/post", async (req, res, ctx) => {
  // req.body is the original body (native browser FormData). The modified body (with boundary value) returned from the service worker is ignored
  // req.headers is the original headers
})

Node

rest.post("/post", async (req, res, ctx) => {
  // req.body is the original body (whatever FormData polyfill the user installed)
  // req.headers is the original headers
})
0reactions
delijahcommented, Mar 8, 2022

I’ve investigated some more here…

  • It might be possible to use File and FormData from jsdom (if this is anyway already a dependency)
  • Or, what we are using at the moment:
const extractFilesFromMultipartBody = (req) =>
    new Promise((resolve) => {
        const buffer = Buffer.from(req.body, 'hex');
        const bb = busboy({ headers: req.headers.all() });
        const files = [];

        bb.on('file', (name, file, info) => {
            const { filename, mimeType } = info;
            const buffers: Buffer[] = [];

            file.on('data', (data) => {
                buffers.push(data);
            }).on('close', () => {
                const buffer = Buffer.concat(buffers);

                files.push({
                    buffer,
                    name: filename,
                    type: mimeType,
                });
            });
        });

        bb.on('close', () => {
            resolve(files);
        });

        bb.end(buffer);
    });

It looks like msw is currently converting req.body to a hex-string for multipart requests, when running in a node.js environment.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Including JavaScript class definition from another file in ...
You can simply do this: user.js class User { //... } module.exports = User // Export class. server.js const User = require('.
Read more >
How To Work with Files using the fs Module in Node.js
With Node.js, you can use JavaScript to programmatically manipulate files with the built-in fs module. The name is short for “file system,” ...
Read more >
File system | Node.js v19.3.0 Documentation
On Linux, positional writes don't work when the file is opened in append mode. The kernel ignores the position argument and always appends...
Read more >
Node.js File System Module
The Node.js file system module allows you to work with the file system on your computer. ... If the file does not exist,...
Read more >
import - JavaScript - MDN Web Docs - Mozilla
js file containing the module. In Node, extension-less imports often refer to packages in node_modules . Certain bundlers may permit importing ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found