http-request-abort
v1.0.0
Published
> Documentation for the `http-request-abort` middleware and helpers. This package provides abort functionality for HTTP requests in Node.js servers.
Readme
http-request-abort
Documentation for the
http-request-abortmiddleware and helpers. This package provides abort functionality for HTTP requests in Node.js servers.
Table of contents
http-request-abort- Table of contents
- Overview
- When to use
- Installation
- Quick example (Express)
- Quick example (native
httpserver) - Detailed API
- Options & defaults
- Behavior and lifecycle
- Events listened & cleanup guarantees
- Edge cases & gotchas
- Testing guidance
- Compatibility & runtime notes
- Best practices and security considerations
- Full TypeScript signatures (reference)
- Example: robust handler pattern
- Contributing
- License
- Author
Overview
This package adds a small, safe AbortController-based abort mechanism to incoming HTTP requests. The middleware:
- attaches an
AbortSignaltoreq.signal(if one is not already present), - listens for request/response lifecycle events that should cancel in-flight operations (request
close, requesterror, responsefinish/close/error), - optionally enforces a timeout,
- optionally emits an
'abort'event on the request, - exposes helpers:
isAborted,getAbortReason, andraceWithAbort.
It is intentionally conservative: it will not overwrite an existing req.signal.
When to use
- You have long-running async work per-request (database queries, remote API calls, streams) and want a standard way to cancel that work if the client disconnects, the response finishes, or a timeout occurs.
- You want consistent abort semantics across Express/Connect-style middleware or raw
httpservers. - You want to centralize logging and abort reason handling.
Installation
npm install http-request-abortQuick example (Express)
import express from "express";
import abort, {
HttpRequestAbortError,
raceWithAbort,
} from "http-request-abort";
const app = express();
// Add middleware early
app.use(abort({ timeout: 5000, logger: console.log }));
app.get("/slow", async (req, res) => {
try {
// use the attached signal in any cancellable operation
const result = await raceWithAbort(fetchSomething(), req.signal, undefined);
res.json(result);
} catch (err) {
if (err instanceof HttpRequestAbortError) {
// client disconnected or timed out
res.status(499).end("Client aborted");
return;
}
res.status(500).end("Server error");
}
});Quick example (native http server)
import http from "http";
import abort, { getAbortReason, isAborted } from "http-request-abort";
const handler = (req, res) => {
// emulate express-like next
const next = (err) => {
if (err) {
res.statusCode = 500;
res.end(String(err.message ?? err));
return;
}
// Handler continues...
// Use req.signal for cancellable ops
someAsyncWork(req.signal)
.then((v) => {
if (!isAborted(req)) res.end("OK");
else res.end("Aborted");
})
.catch((err) => res.end("err"));
};
// apply middleware then continue to handler logic
abort({ timeout: 2000 })(req, res, next);
};
http.createServer(handler).listen(3000);Detailed API
Types & interfaces
AbortPhase = "initialization" | "request" | "response"Where the abort happened (used byHttpRequestAbortError).AbortContext{ readonly url?: string; readonly cause?: string; readonly phase: AbortPhase; readonly timestamp: number; }Context object created when aborting (useful for logging).
Logger(message: string, context?: Record<string, any>) => voidCustom logger function accepted via options.AbortOptions{ readonly failOnRequestClosed?: boolean; readonly failOnResponseClosed?: boolean; readonly reason?: any; readonly timeout?: number; readonly logger?: Logger; readonly emitAbortEvent?: boolean; }Options passed to
abort()(see Options & defaults).
HttpRequestAbortError
Extends
Error.Additional properties:
code: "HTTP_REQUEST_ABORTED"timestamp: number(when the error instance was created)cause?: stringphase?: AbortPhase
toJSON()returns an object with{ name, message, code, cause, phase, timestamp }.
Usage notes:
- The middleware uses
HttpRequestAbortErrorwhen it needs to report a fail-fast abort during initialization (callsnext(err)with this error). raceWithAbortalso rejects withHttpRequestAbortErrorwhen a timeout or AbortSignal abort occurs.
Default export: abort(options?: AbortOptions)
Signature
export default function abort(
options?: AbortOptions
): (
req: IncomingMessage,
res: ServerResponse,
next: (err?: any) => void
) => void;Behavior
If
req.signalalready exists, middleware logs that it's skipping (ifloggerprovided) and callsnext()immediately — it will not overwrite an existing signal.Fail-fast checks:
- If
failOnRequestClosedistrue(default) and the request is already closed (req.destroyed || req.complete), it immediately callsnext(new HttpRequestAbortError(...)). - If
failOnResponseClosedistrue(default) and the response is already closed (res.writableEnded || res.closed), it immediately callsnext(new HttpRequestAbortError(...)).
- If
Otherwise:
Constructs a new
AbortController, setsreq.signal = controller.signal.Registers listeners:
req.once("close")=> abortreq.once("error")=> abort with messageres.once("finish")=> abort (scheduled viasetImmediate)res.once("close")=> abortres.once("error")=> abort with messagecontroller.signal.addEventListener("abort")=> abort (covers external aborts of the controller)
Optionally sets a
timeoutthat will call abort when expired.Optionally
emitAbortEvent: whentrueand the request is not destroyed, the middleware willreq.emit("abort")on abort.The middleware ensures listeners that it added are removed via
removeListenerwhen aborting or on cleanup.If
next()throws synchronously, the middleware callsdoAbort(...)then rethrows the error (preserving original behavior).
Note: the middleware expects next to be an Express/Connect-style continuation function. For raw http usage, you can pass a wrapper next that continues request handling.
Utilities
isAborted(req: IncomingMessage): booleanReturns!!req.signal?.aborted.getAbortReason(req: IncomingMessage): anyReturnsreq.signal?.reason(if present) — the value passed toAbortController.abort(reason).raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal, timeoutMs?: number): Promise<T>Racespromiseagainst:signalabort: rejects withHttpRequestAbortError("Operation aborted", reason)wherereason = signal.reason ?? "Signal aborted".timeoutMsexpiration: rejects withHttpRequestAbortError("Operation timed out", 'Timeout after ${timeoutMs}ms').- If neither
signalnortimeoutis supplied, returns the originalpromiseas-is.
Important: raceWithAbort cleans up listeners/timers after settling and ensures the first to settle wins. If the underlying promise rejects, that rejection is preserved.
Options & defaults
| Option | Type | Default | Description |
| ---------------------- | -------------------: | ----------: | -------------------------------------------------------------------------------------------------------------------------------- |
| failOnRequestClosed | boolean | true | If true, fail fast (call next with HttpRequestAbortError) when the request is already closed during middleware initialization. |
| failOnResponseClosed | boolean | true | If true, fail fast when the response is already closed during middleware initialization. |
| reason | any | undefined | If provided, used as the reason when calling controller.abort(reason) (instead of default textual cause). |
| timeout | number | 0 | Milliseconds to wait before automatically aborting the request. 0 or falsy disables timeout. |
| logger | (msg, ctx) => void | undefined | Logger function called for lifecycle events (safe: errors in logger are caught by the middleware). |
| emitAbortEvent | boolean | false | If true, the middleware will emit an 'abort' event on the req when aborting (if req is not destroyed). |
Behavior and lifecycle
Initialization
- Middleware first checks if
req.signalexists: if yes, it skips and callsnext(). - Then it performs fail-fast checks on request/response closed state.
- Middleware first checks if
Attachment
- Creates
AbortController, setsreq.signaltocontroller.signal. - Adds listeners for request/response events and for
controller.signalabortevent.
- Creates
Abort triggers (examples)
- Client disconnects (
reqemitsclose) → abort. - Request emits
error→ abort with error message. - Response
finish(completed successfully) → abort (scheduled withsetImmediate). - Response
closeorerror→ abort. timeoutreached → abort with timeout message.- External call to the same controller (ifAccessible) →
controller.signalabortevent triggers abort logic.
- Client disconnects (
When aborting
- The middleware calls
controller.abort(reason)unless already aborted. - Calls
logger?.("aborting request", ctx)wherectxisAbortContext. - Optionally emits
req.emit("abort")ifemitAbortEventis true andreqis not destroyed. - Cleans up timers and removes only the listeners it added.
- The middleware calls
Synchronous errors from
next()- If
next()throws synchronously, middleware callsdoAbort(...)and then rethrows the error, so upstream still sees the exception.
- If
Events listened & cleanup guarantees
Events registered
On
req:close(once)error(once)
On
res:finish(once)close(once)error(once)
On
controller.signal:abort(listener)
Cleanup
- The middleware removes only the listeners it added by calling
removeListeneron each named callback and clears timeout if set. This minimizes interference with other code using the samereq/resevents.
Edge cases & gotchas
- Existing
req.signal: The middleware does not overwrite an existingreq.signal. If your application or another middleware already setreq.signal, this middleware will skip (and log if a logger is present). AbortSignal.reasonavailability: Some Node.js versions exposeAbortSignal.reason(when abort is called with a reason). If your Node version does not supportsignal.reason,getAbortReasonmay beundefined. UsegetAbortReasonhelper to access the reason (it readsreq.signal?.reason).res.finishscheduling: The middleware treatsres.finishas a natural completion and triggers abort on the next tick (setImmediate) to avoid interfering with synchronous post-finish tasks.- Races at initialization: The middleware checks request/response closed state and also schedules a
setImmediaterace-check for the response to handle synchronous state changes that happen during setup. - Logger safety: Errors thrown inside your provided
loggerwill be swallowed so that logging cannot break request flow. emitAbortEventcaution: Emitting'abort'on the request may cause other code listening for it to run. Only setemitAbortEvent: trueif you want that extra behaviour.- Memory leaks: The implementation removes its listeners when aborting or cleaning up. However, if your
next()never returns and you never reach abort/finish/close, a timeout can be used to ensure cleanup.
Testing guidance
Unit-test the middleware with lightweight mocks that emulate
IncomingMessageandServerResponse(both extendEventEmitterin Node). You only needemit,once,removeListener, and the properties the middleware inspects (url,destroyed,complete,writableEnded,closed).Test the following scenarios:
req.signalalready present → middleware does nothingreq.destroyedorres.writableEndedduring init → fail-fastHttpRequestAbortErrorvianext(err)timeouttriggered →req.signal.aborted === trueandgetAbortReasoncontains the timeout causereq.emit('close')triggers abort and (ifemitAbortEvent)req.emit('abort')res.emit('finish')triggers abort (due tosetImmediate)next()throws synchronously → middleware aborts then rethrows errorraceWithAbort: resolves normally, underlying rejection passes through, timeout causesHttpRequestAbortError,signal.abort()causesHttpRequestAbortError
Use Node's
node:testor your favorite test runner. MockAbortControlleronly if your runtime doesn't support it (Node >= 16.7+ includesAbortController).
Compatibility & runtime notes
- Requires a Node.js runtime with
AbortController/AbortSignal(Node 16.7+ has built-inAbortController; newer Node versions addAbortSignal.reasonsupport). - The module interacts with
req/resobjects that have Nodehttpsemantics (IncomingMessageandServerResponse). It was designed to be compatible with Express/Connect and raw Nodehttpservers. - When compiling/transpiling TypeScript for use in Node, ensure the module format (ESM vs CommonJS) matches your consuming environment.
Best practices and security considerations
- Do not rely on
emitAbortEventunless you control listeners — it will trigger any listener onreqfor'abort'. - Provide a logger for production so you can trace abort reasons and durations; the middleware logs useful context when available.
- Prefer
raceWithAbortwhen awaiting cancellable operations so resources are released as soon as a client disconnects or a timeout occurs. - Avoid long timeouts unless necessary. A conservative request timeout prevents resource exhaustion under heavy load.
- Handle
HttpRequestAbortErrorgracefully in async handlers — most aborts are normal (client disconnects, timeouts) and should not crash the process. Return appropriate status codes or simply stop work and close resources.
Full TypeScript signatures (reference)
// Types
export type AbortPhase = "initialization" | "request" | "response";
export interface AbortContext {
readonly url?: string;
readonly cause?: string;
readonly phase: AbortPhase;
readonly timestamp: number;
}
export type Logger = (message: string, context?: Record<string, any>) => void;
export interface AbortOptions {
readonly failOnRequestClosed?: boolean;
readonly failOnResponseClosed?: boolean;
readonly reason?: any;
readonly timeout?: number;
readonly logger?: Logger;
readonly emitAbortEvent?: boolean;
}
// Exports
export class HttpRequestAbortError extends Error {
public readonly code: "HTTP_REQUEST_ABORTED";
public readonly timestamp: number;
constructor(message: string, cause?: string, phase?: AbortPhase);
toJSON(): {
name: string;
message: string;
code: string;
cause?: string;
phase?: AbortPhase;
timestamp: number;
};
}
export default function abort(
options?: AbortOptions
): (
req: IncomingMessage,
res: ServerResponse,
next: (err?: any) => void
) => void;
export const isAborted: (req: IncomingMessage) => boolean;
export const getAbortReason: (req: IncomingMessage) => any;
export async function raceWithAbort<T>(
promise: Promise<T>,
signal?: AbortSignal,
timeoutMs?: number
): Promise<T>;Example: robust handler pattern
import abort, {
HttpRequestAbortError,
raceWithAbort,
} from "http-request-abort";
app.use(abort({ timeout: 10000, logger: (m, c) => console.debug(m, c) }));
app.get("/data", async (req, res) => {
try {
const data = await raceWithAbort(fetchFromRemote(), req.signal, undefined);
res.json(data);
} catch (err) {
if (err instanceof HttpRequestAbortError) {
// abort was triggered by client disconnect or timeout:
// no need to call res.end if connection is gone — safe fallback:
if (!res.writableEnded) {
res.status(499).end("Client aborted");
}
return;
}
// Handle other errors
res.status(500).end("Internal error");
}
});Contributing
Contributions are welcome! Feel free to open issues or submit pull requests.
License
MIT License. See LICENSE for details.
Author
Made with ❤️ by Rayen Boussayed
