http.IncomingMessage
Represents an incoming request on the server or a response on the client as a readable stream.
Class: http.IncomingMessage extends <stream.Readable>- Added in Node.js v0.1.17
Parameters
| Parameter | Type | Description |
|---|---|---|
$socket | stream.Duplex | The 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.
| Property | Populated on | Notes |
|---|---|---|
method, url | server requests | url is the raw request target only — path and query, never scheme or host |
statusCode, statusMessage | client responses | undefined on the server side |
headers | both | names lower-cased; duplicates collapsed |
rawHeaders | both | flat array, original case, nothing merged |
complete | both | false 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.urlis not a URL. It is/path?query. Build a real one withnew URL(req.url, \`http://${req.headers.host}\`).- Header names are lower-cased, so
headers['Content-Type']is alwaysundefined. - Duplicate headers are collapsed — the second
content-typeis discarded, whileset-cookieis special-cased into an array. UserawHeadersorheadersDistinctwhen duplicates matter. 'end'does not mean success. Checkmessage.complete; a truncated upload still ends.
Example
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: 0When 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.
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.
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