http.request()

Issues an outgoing HTTP request and returns a writable ClientRequest stream for the body.

Signature
http.request(url[, options][, callback])
  • Added in Node.js v0.3.6

Parameters

ParameterTypeDescription
$urlstring | URLTarget 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]).
$optionsoptionalObjectMerged 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.
$callbackoptionalFunctionAdded 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 location header.
  • HTTPS URLs need https.request(); this module speaks plaintext only.

Example

Example · javascript
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.

Example · javascript
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.

Example · javascript
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

  • Be the first to comment on this lesson.