on-headers
Fire a listener right before a Node.js HTTP response writes its headers.
Repository Health
Technical Analysis
on-headers is a tiny, zero-dependency Node.js module that lets other middleware hook into the exact moment an HTTP response is about to send its headers. It works by wrapping res.writeHead, so a registered listener runs with the response object as its this context right before headers are flushed to the client, giving code one last chance to inspect or modify headers set anywhere earlier in the request lifecycle.
Because frameworks and middleware often need to add or adjust headers late (compression negotiating Content-Encoding, ETag/cache middleware finalizing validators, logging middleware capturing what was actually sent), on-headers has become a foundational building block in the Node.js HTTP ecosystem rather than something applications typically depend on directly.
What You Get
- Single hook point - one
onHeaders(res, listener)call attaches a function that fires right before headers leave the server, regardless of which code path triggers the response. res.writeHeadinterception - transparently wraps the existingwriteHeadmethod so array, object, and positional header arguments are all normalized and preserved.- Multiple listener stacking - calling
onHeadersmore than once on the same response chains listeners, firing them in reverse order of registration. - Status code override support - a listener can change
res.statusCodeinside its callback and on-headers propagates that change into the finalwriteHeadcall.
Common Use Cases
- Response compression - libraries like
compressionuse on-headers to setContent-Encodingonly after confirming a body will actually be compressed. - ETag/conditional-GET middleware - generate and attach validators just before headers are sent, once the full response body is known.
- Request logging -
morgan-style loggers capture final header state (likeContent-Length) that other middleware only sets late in the request lifecycle. - Security header injection - middleware that needs to guarantee a header is present or corrected regardless of what earlier handler code did or didn’t set.
Under The Hood
Architecture
The module is a single file (index.js) whose entire surface is one exported function, onHeaders(res, listener), which wraps res.writeHead with a closure produced by createWriteHead. That closure captures the previous writeHead, the listener, and a fired flag guaranteeing the listener runs exactly once even if writeHead is invoked more than once. Header normalization is handled separately in setWriteHeadHeaders, which detects whether headers were passed as a 2D array, 1D array, or object and dispatches to set2dArray, set1dArrayWithAppend, or set1dArrayWithSet accordingly, choosing the append-based path when the Node runtime exposes res.appendHeader. There is no internal state beyond what’s attached to the response object itself, so the only thing that could break this abstraction is a change to how Node’s own writeHead/appendHeader behave internally — a risk the project explicitly guards against with a dedicated test/upstream.js suite that hashes and re-checks Node’s internal functions.
Tech Stack
Plain, untranspiled JavaScript targeting "engines": { "node": ">= 0.8" }, with zero runtime dependencies. Dev tooling is ESLint (eslint-config-standard) for style, Mocha for tests, nyc for coverage, and supertest for spinning up real http.Server instances in tests. The package ships only index.js, LICENSE, HISTORY.md, and README.md per its files allowlist — there is no build step. CI runs the full test suite across a very wide Node.js version matrix (0.8 through the latest release) plus a separate upstream-hash workflow that opens a GitHub issue automatically if Node core changes the functions this module patches.
Code Quality
test/test.js (469 lines) exercises the module end-to-end against real HTTP servers via supertest rather than mocking res, covering array/object header forms, multiple stacked listeners, status-code overrides, and edge cases like malformed header arrays. A second, unusual test file (test/upstream.js plus test/known-upstream-hashes.json) hashes the Node.js core functions the module depends on and fails CI if they drift — a defensive practice rarely seen in packages this small. ESLint enforces standard style plus no-param-reassign, and npm run test-ci wires coverage via nyc. There is no TypeScript and no type annotations anywhere in the source, which is the main gap relative to a fully modern JS/TS toolchain.
What Makes It Unique on-headers doesn’t attempt to be more than a narrow, boring, single-purpose monkey-patch, and that restraint is deliberate: it prioritizes maximal compatibility across an extremely wide range of Node.js versions over adding capability. Its one genuinely distinctive technical choice is the upstream-hash self-check that proactively detects when Node’s own internals change underneath the patch it relies on, automatically filing a maintenance issue rather than failing silently in consumers’ production servers.
Used by 5 apps in this directory
AFFiNE
Productivity · Project Management · Note Taking
Write, draw, and plan in one infinite canvas — the open-source alternative to Notion and Miro that keeps your data yours.
Ghost
CMS · Blogging
Open source headless Node.js CMS for professional publishing, paid memberships, and newsletters with a fully owned audience.
Huly Platform
Project Management · Team Chat · Collaboration
Open-source all-in-one workspace that replaces Linear, Jira, Slack, and Notion for product and engineering teams.
HyperDX
Developer Tools · Analytics · Monitoring
Open source observability platform that unifies logs, traces, metrics, and session replays on ClickHouse — now the core of ClickStack.
overleaf
Collaboration · Productivity
Open-source, real-time collaborative LaTeX editor with sandboxed compilation and full TeXLive support for self-hosted academic and research teams.