http.request()
Issues an outgoing HTTP request and returns a writable ClientRequest stream for the body.
http.request(url[, options][, callback])- Added in Node.js v0.3.6
Parameters
| Parameter | Type | Description |
|---|---|---|
$url | string | URL | Target URL. A string is parsed with new URL(); a URL object is converted to an options object. May be omitted in favour of the options-only overload, http.request(options[, callback]). |
$optionsoptional | Object | Merged over anything parsed from url, taking precedence. Common keys: method (default 'GET'), path (default '/'), host (default 'localhost'), port (default 80), protocol (default 'http:'), headers, auth, agent, signal, timeout, socketPath, setHost, insecureHTTPParser (default false), joinDuplicateHeaders (default false), maxHeaderSize (default 16384). Options accepted by socket.connect() are supported too. |
$callbackoptional | Function | Added as a one-time listener for the 'response' event. Receives the http.IncomingMessage. |
Return value
<http.ClientRequest>A ClientRequest, which is a writable stream. Write the body to it, then call request.end() — the request is not sent until end() is called.
Errors
Throws synchronously if the request path contains illegal characters (currently spaces); DNS, TCP, and HTTP parse failures are emitted as an 'error' event on the returned ClientRequest, and an unhandled 'error' event crashes the process.
Changelog
- PHP v16.7.0, v14.18.0A username and password in a URL object are now properly URI-decoded before being used for basic auth.
- PHP v15.3.0, v14.17.0A request can be aborted with an AbortSignal via the signal option.
- PHP v13.8.0, v12.15.0, v10.19.0The insecureHTTPParser option is supported.
- PHP v10.9.0The url parameter can be passed alongside a separate options object, which are then merged.
http.request() is the low-level client. It returns a writable stream rather than a promise, which is why the shape looks unfamiliar next to fetch(): you write the body into the request, and you read the body out of the response in a separate callback.
When to reach for it
Global fetch() covers most application code now. Reach for http.request() when you need something fetch does not expose: a Unix socket (socketPath, the way you talk to the Docker daemon), fine-grained agent and connection-pool control, raw duplicate headers, an upload streamed straight from disk without buffering, or per-request local address binding.
What trips people up
- You must call
request.end(). Always — even with no body. Without it the request is never dispatched and nothing happens, with no error. - The callback does not give you the body. It gives you an
IncomingMessage; you still have to collect'data'chunks and wait for'end'. - Unconsumed responses leak. If you ignore the body, call
res.resume()to drain it, or the socket is never returned to the agent pool. - Redirects are not followed. A 301 is just a response with a
locationheader. - HTTPS URLs need
https.request(); this module speaks plaintext only.
Example
const http = require('node:http');
const payload = JSON.stringify({ msg: 'Hello World!' });
const req = http.request('http://127.0.0.1:8000/upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Byte length, not string length -- multibyte bodies差 otherwise.
'Content-Length': Buffer.byteLength(payload),
},
}, (res) => {
console.log('STATUS:', res.statusCode);
res.setEncoding('utf8');
let body = '';
res.on('data', (chunk) => { body += chunk; });
res.on('end', () => {
console.log('BODY:', body);
});
});
req.on('error', (err) => {
console.log('request failed:', err.code);
});
req.write(payload);
req.end(); // required -- nothing is sent without it
// STATUS: 200
// BODY: {"ok":true}When to use it
- Talk to the Docker or systemd API over a Unix socket, which fetch() cannot do
- Stream a large file upload from disk without buffering it in memory
- Send a webhook from a service that must stay dependency-free
- Set a per-request agent to control keep-alive and max sockets against a slow upstream
Frequently asked questions
Should I use http.request() or fetch() in Node?
Use fetch() for ordinary API calls — it is global since Node 18, returns promises, and follows redirects. Use http.request() when you need Unix sockets, agent and connection-pool control, raw duplicate headers, or a streamed upload, none of which fetch exposes.
Why does my http.request() never fire the callback?
You almost certainly did not call request.end(). Node treats the ClientRequest as an open writable stream and does not dispatch the request until end() signals the body is complete — this applies even to GET requests with no body.
How do I send a POST request with JSON in Node without axios?
Stringify the body, set Content-Type: application/json and a Content-Length computed with Buffer.byteLength(), then req.write(payload) followed by req.end(). Using payload.length instead of Buffer.byteLength() truncates any body containing non-ASCII characters.
More examples
Streaming a download to disk without buffering
The response is a readable stream, so piping it straight to a file keeps memory flat regardless of download size. Note the res.resume() on the error path: an IncomingMessage you never read holds its socket open and, with keep-alive, blocks the agent's connection pool.
import http from 'node:http';
import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';
const res = await new Promise((resolve, reject) => {
const req = http.request('http://example.internal/report.csv', resolve);
req.on('error', reject);
req.end();
});
if (res.statusCode !== 200) {
res.resume(); // drain, or the socket is never released
throw new Error(`unexpected status ${res.statusCode}`);
}
await pipeline(res, fs.createWriteStream('./report.csv'));
console.log('saved', fs.statSync('./report.csv').size, 'bytes');Timeout and cancellation with AbortSignal
The signal option aborts the request wherever it is — DNS, connect, or mid-body — and surfaces as an 'error' event, not a rejection. This differs from the timeout option, which only measures socket inactivity and merely emits 'timeout' without aborting anything, so a request can sit forever if you use timeout alone.
const http = require('node:http');
const req = http.request('http://slow.internal/data', {
signal: AbortSignal.timeout(2000), // whole-request deadline
}, (res) => {
res.resume();
res.on('end', () => console.log('done', res.statusCode));
});
req.on('error', (err) => {
if (err.name === 'AbortError' || err.name === 'TimeoutError') {
console.log('gave up after 2s');
} else {
console.log('failed:', err.code);
}
});
req.end();
Discussion