http.IncomingMessage

Represents an incoming request on the server or a response on the client as a readable stream.

Signature
Class: http.IncomingMessage extends <stream.Readable>
  • Added in Node.js v0.1.17

Parameters

ParameterTypeDescription
$socketstream.DuplexThe socket the message was parsed from. Passed by Node itself; you only touch this when subclassing IncomingMessage for the createServer() option of the same name.

Return value

<http.IncomingMessage>Never constructed by application code. Node creates one and passes it as the first argument to a server's 'request' listener and as the argument to a client's 'response' callback.

Errors

Reading a property never throws; an aborted or malformed message emits an 'error' or 'aborted' event on the stream instead.

Changelog

  • PHP v16.5.0Added message.headersDistinct and message.trailersDistinct, which give every header as an array instead of collapsing duplicates.
  • PHP v15.5.0message.destroyed now returns true once the incoming data has been consumed.
  • PHP v13.1.0, v12.16.0readableHighWaterMark now mirrors that of the underlying socket.

An IncomingMessage is the parsed head of an HTTP message plus a readable stream of its body. The same class serves both directions, which is why some properties are only ever populated on one side.

PropertyPopulated onNotes
method, urlserver requestsurl is the raw request target only — path and query, never scheme or host
statusCode, statusMessageclient responsesundefined on the server side
headersbothnames lower-cased; duplicates collapsed
rawHeadersbothflat array, original case, nothing merged
completebothfalse at 'end' means the peer cut the connection early

What trips people up

  • There is no .body. Read the stream, or use a body parser.
  • req.url is not a URL. It is /path?query. Build a real one with new URL(req.url, \`http://${req.headers.host}\`).
  • Header names are lower-cased, so headers['Content-Type'] is always undefined.
  • Duplicate headers are collapsed — the second content-type is discarded, while set-cookie is special-cased into an array. Use rawHeaders or headersDistinct when duplicates matter.
  • 'end' does not mean success. Check message.complete; a truncated upload still ends.

Example

Example · javascript
const http = require('node:http');

const server = http.createServer((req, res) => {
  // req.url is only the request target: '/search?q=node&page=2'
  const url = new URL(req.url, `http://${req.headers.host}`);

  console.log('method:', req.method);
  console.log('pathname:', url.pathname);
  console.log('q:', url.searchParams.get('q'));
  console.log('httpVersion:', req.httpVersion);
  // Header names are always lower-cased.
  console.log('user-agent:', req.headers['user-agent']);

  const chunks = [];
  req.on('data', (chunk) => chunks.push(chunk));
  req.on('end', () => {
    // 'end' fires even on a truncated body -- check complete.
    console.log('complete:', req.complete);
    console.log('bytes:', Buffer.concat(chunks).length);
    res.writeHead(204).end();
  });
});

server.listen(3000);
// method: GET
// pathname: /search
// q: node
// httpVersion: 1.1
// user-agent: curl/8.4.0
// complete: true
// bytes: 0

When to use it

  • Read a JSON or form-encoded POST body from a plain Node server
  • Route on req.method and the pathname parsed out of req.url
  • Inspect rawHeaders to preserve duplicate or case-sensitive headers a proxy sent
  • Detect truncated uploads by checking message.complete before persisting the body

Frequently asked questions

How do I get the request body in Node without Express?

Read the IncomingMessage as a stream — either for await (const chunk of req) or the 'data'/'end' event pair — then Buffer.concat the chunks. There is no req.body property; body parsing is exactly what middleware like express.json() does for you.

Why is req.headers['Content-Type'] undefined?

Node lower-cases every incoming header name, so the key is 'content-type'. HTTP header names are case-insensitive on the wire, and normalising them lets lookups be consistent; use req.rawHeaders if you need the sender's original casing.

Why does req.url only contain the path and not the full URL?

req.url is the raw HTTP request target, which for a normal request is just the path and query string — the host travels in the Host header. Reconstruct a full URL with new URL(req.url, `http://${req.headers.host}`).

More examples

The same class on the client side

Demonstrates the split personality of the class: statusCode/statusMessage on a response, method/url on a request, never both. It also shows the two header views — the lower-cased, de-duplicated headers object versus the flat rawHeaders array that keeps original casing and every repeated field.

Example · javascript
const http = require('node:http');

http.get('http://127.0.0.1:8000/api', (res) => {
  // On a response, method and url are undefined;
  // statusCode and statusMessage are set instead.
  console.log(res.statusCode, res.statusMessage); // 200 OK
  console.log(res.method, res.url);               // undefined undefined

  // headers collapses duplicates; rawHeaders preserves everything.
  console.log(res.headers['content-type']);
  console.log(res.rawHeaders.slice(0, 2));        // ['Content-Type', 'application/json']

  // set-cookie is the one header always exposed as an array.
  console.log(Array.isArray(res.headers['set-cookie']));

  res.resume();
}).on('error', (err) => console.log(err.code));

Collecting a body safely, with a size cap and truncation check

Because IncomingMessage extends stream.Readable it is async-iterable, so for await replaces the 'data'/'end' listener pair. The two guards are what make this production-ready: a byte cap so a hostile client cannot exhaust memory, and a req.complete check so a connection dropped mid-upload is rejected rather than stored as a valid short body.

Example · javascript
import http from 'node:http';

const MAX_BYTES = 1_000_000;

async function readBody(req) {
  let size = 0;
  const chunks = [];
  for await (const chunk of req) {   // IncomingMessage is async-iterable
    size += chunk.length;
    if (size > MAX_BYTES) {
      req.destroy();
      throw new Error('payload too large');
    }
    chunks.push(chunk);
  }
  if (!req.complete) throw new Error('client disconnected mid-body');
  return Buffer.concat(chunks);
}

const server = http.createServer(async (req, res) => {
  try {
    const body = await readBody(req);
    console.log('received', body.length, 'bytes');
    res.writeHead(200).end();
  } catch (err) {
    // An async handler MUST catch: an unhandled rejection kills the process.
    res.writeHead(err.message === 'payload too large' ? 413 : 400).end();
  }
});

server.listen(3000);

Discussion

  • Be the first to comment on this lesson.