http: add req.signal to IncomingMessage by akshatsrivastava11 · Pull Request #62541 · nodejs/node

Fixes: #62481

Summary

Adds a lazy signal getter to IncomingMessage that returns an
AbortSignal which aborts when the request is closed or aborted.
This mirrors the Web Fetch API's Request.signal and Deno's
request.signal.

Motivation

Currently, cancelling async work (DB queries, fetch calls) when a
client disconnects requires manual boilerplate in every request handler:

// before
server.on('request', async (req, res) => {
  const ac = new AbortController();
  req.on('close', () => ac.abort());
  const data = await fetch('https://slow-api.com', { signal: ac.signal });
  res.end(JSON.stringify(data));
});

With this change:

// after
server.on('request', async (req, res) => {
  const data = await fetch('https://slow-api.com', { signal: req.signal });
  res.end(JSON.stringify(data));
});

Design

Lazy initializationAbortController is only created when
req.signal is first accessed. Zero overhead for handlers that
do not use it.

Single listener — listens on this.once('close') and
this.once('abort') rather than socket.once('close') directly,
since the request stream's close event fires in all teardown paths:

  • socket close propagates through _destroy() to req close
  • socketless req.destroy() fires req close directly
  • client abort fires req abort directly

Race condition — if .signal is accessed after req.destroy()
has already fired, this.destroyed is checked and the signal is
aborted immediately rather than registering a listener that would
never fire.

configurable: true — allows frameworks (Express, Fastify, Koa)
to override the property on their own subclasses.

Changes

  • lib/_http_incoming.js — adds signal getter to IncomingMessage
  • test/parallel/test-http-request-signal.js — adds tests

Tests

  • req.signal is an AbortSignal and not aborted initially
  • signal aborts when 'abort' event fires
  • signal aborts when 'close' event fires (client disconnect)
  • signal is pre-aborted if accessed after req.destroy() (race condition)
  • multiple accesses return the same signal instance (lazy init)

Fixes: #62481