Requests and Responses

Parsing Requests and Streaming Responses

Your listener runs when the headers are in, so the body is still arriving. IncomingMessage is a readable stream: drain it with for await, and reject it with a byte counter, without which one POST can buy all the memory in the process. The listing imports from node:http, node:zlib, node:stream and node:stream/promises.

A size-limited body reader and a streamed, compressed responseJavaScript
async function readJson(req, limit = 64 * 1024) {
  const chunks = []; let size = 0;
  for await (const chunk of req) {
    if ((size += chunk.length) > limit) { req.destroy(); throw new Error('too large'); }
    chunks.push(chunk);
  }
  return JSON.parse(Buffer.concat(chunks).toString('utf8'));
}
function* rows(n) { for (let i = 1; i <= n; i++) yield `${i},Book ${i}\n`; }
createServer(async (req, res) => {
  if (req.method === 'POST') {
    res.writeHead(201, { 'content-type': 'application/json' });
    return res.end(JSON.stringify({ received: Object.keys(await readJson(req)) }));
  }
  res.writeHead(200, { 'content-type': 'text/csv', 'content-encoding': 'gzip' });
  await pipeline(Readable.from(rows(50_000)), createGzip(), res);
}).listen(3001, '127.0.0.1');
Output
HTTP/1.1 201 Created
Transfer-Encoding: chunked
{"received":["title","tags"]}

The CSV route generates 827,788 bytes and sends 227,411 over the wire, while memory holds only one chunk: pipeline propagates backpressure from the socket back through gzip to the generator, and destroys every stage if the client disconnects. Node adds Transfer-Encoding: chunked to any body written without a Content-Length.