node.jsexpress

fs.createReadStream(path).pipe(res) doesn't work if Content-Length is set


I have the following route in my server:

router.get('*', authz.validateAccount, (request, response) => {
    const accountId = request.user.accountId;
    const path = shared.resolveDataPath(decodeURI(request.url), accountId);
    const stat = fs.statSync(path, { throwIfNoEntry: false, bigint: true });

    // Make sure it's a valid file
    if (stat == undefined || stat.isDirectory()) {
        shared.errorResponse(response, 404, 'The requested URL was not found on this server.');
        return;
    }

    // Upload the file to the client
    response.setHeader('Content-Type', mime.getType(path));
    response.setHeader('Content-Length', stat.size);
    fs.createReadStream(path).pipe(response);
});

However this has now started failing out of the blue after having worked fine for years. I noticed that the upload attempt stops immediately and the connection is closed. After much head scratching I found out that the upload starts working again if I remove the content length header.

What I'm wondering is if I'm doing something wrong here? aren't I'm supposed to set the content length? Note that there is no compression or anything going on, so the content length header should be correct. Is this a bug in Node?


Solution

  • When reading size with fs.statSync, in options you're passing bigint: true, so it returns numeric values as BigInt.

    But setting content-length Express/HTTP expects a Number/String, which is why it fails (you should see TypeError: Cannot convert a BigInt value to a number somewhere in your logs/error handlers).

    So, you either remove bigint: true, or convert size to number/string (in that case note: Number.MAX_SAFE_INTEGER, also, omitting content-length header defaults to Transfer-Encoding: chunked):

    response.setHeader('Content-Length', Number(stat.size));// or String(stat.size)
    

    see: fs.statSync