node.jshttpbuffernodejs-streamhttp-range

How to stream range http requests from buffers in NodeJS


Testing http range requests in node, when we stream from

fs.createReadStream('index.html').pipe(res)

to a http res of a http server, the http client from node accepts it.

But, when I pipe a Stream of a Buffer, with:

const content = fs.readFileSync("src/index.html");
const stream = new Readable();

stream.push(
  opts.start && opts.end
    ? content.slice(opts.start, opts.end + 1)
    : content
);
stream.push(null);

stream.pipe(res);

Curl and browsers accept it, except NodeJS http client, that throws:

events.js:180
      throw er; // Unhandled 'error' event
      ^

Error: Parse Error
    at Socket.socketOnData (_http_client.js:452:22)
    at Socket.emit (events.js:203:13)
    at addChunk (_stream_readable.js:294:12)
    at readableAddChunk (_stream_readable.js:275:11)
    at Socket.Readable.push (_stream_readable.js:210:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:166:17)
Emitted 'error' event at:
    at Socket.socketOnData (_http_client.js:458:9)
    at Socket.emit (events.js:203:13)
    [... lines matching original stack trace ...]
    at TCP.onStreamRead (internal/stream_base_commons.js:166:17) {
  bytesParsed: 234,
  code: 'HPE_INVALID_CONSTANT',
  reason: 'Expected HTTP/'
}

To test it, just change the line 56 from 'read' to 'buffer':

index.js

const http = require("http");
const fs = require("fs");
const Readable = require("stream").Readable;

const stats = fs.statSync("index.html");

const handler = function(read_or_buffer) {
  return function(req, res) {
    let code = 200;
    const opts = {};
    const headers = {
      "Content-Length": stats.size,
      "Content-Type": "text/html",
      "Last-Modified": stats.mtime.toUTCString()
    };

    if (req.headers.range) {
      code = 206;

      let [x, y] = req.headers.range.replace("bytes=", "").split("-");
      let end = (opts.end = parseInt(y, 10) || stats.size - 1);
      let start = (opts.start = parseInt(x, 10) || 0);

      if (start >= stats.size || end >= stats.size) {
        res.setHeader("Content-Range", `bytes */${stats.size}`);
        res.statusCode = 416;
        return res.end();
      }

      headers["Content-Range"] = `bytes ${start}-${end}/${stats.size}`;
      headers["Content-Length"] = end - start + 1;
      headers["Accept-Ranges"] = "bytes";
    }

    res.writeHead(code, headers);

    if (read_or_buffer == "read")
      fs.createReadStream("index.html", opts).pipe(res);

    if (read_or_buffer == "buffer") {
      const content = fs.readFileSync("index.html");
      const stream = new Readable();

      stream.push(
        opts.start && opts.end
          ? content.slice(opts.start, opts.end + 1)
          : content
      );
      stream.push(null);

      stream.pipe(res);
    }
  };
};

http.createServer(handler("read")).listen(8080);

// TESTS

const options = { headers: { Range: "bytes=0-4" } };

http.get("http://127.0.0.1:8080/", options, response => {
  let data = "";
  response.on("data", chunk => (data += chunk));
  response.on("end", () => {
    console.log(data);
    process.exit();
  });
});

index.html

Hello world!

Solution

  • When the server receive a http range request starting from 0:

    opts.start && opts.end
    

    evaluates to false because opts.start is 0 so the code was sending the whole buffer and not the expected slice.

    As NodeJS strictly meets the HTTP specification, it was not accepting the request.


    The solution was to verify, when opts.start exists, if it is zero:

    opts.start || opts.start === 0 && opts.end