handler
The HTTP request handler factory for ergo’s two-accumulator model.
Creates domain accumulator and response accumulator per request, runs the composed
pipeline, catches unexpected errors, and calls send() exactly once.
This is the standalone equivalent of ergo-router’s auto-wrap.
Pipeline stage: Execution (wraps the entire pipeline)
Import
Section titled “Import”import {handler} from '@centralping/ergo';Arguments
Section titled “Arguments”| Argument | Type | Description |
|---|---|---|
pipeline |
function |
Required. Composed middleware pipeline |
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
debug |
boolean |
false |
Enable pipeline debug tracing. When true, responseAcc._trace is initialized with {steps, breakAt} before the pipeline runs. On error responses (≥ 400), _trace appears as an RFC 9457 extension member. |
onResponse |
function |
— | Post-send lifecycle observation hook. Called after send() completes with (req, res, responseInfo, domainAcc). Errors thrown by the hook are swallowed — the hook cannot affect the response. Async hooks are awaited. |
redactHeaders |
Set<string> |
See below | Header names to redact in the onResponse hook’s responseInfo.headers snapshot. Matching headers are replaced with '[REDACTED]'. Pass an empty Set to disable redaction. Shares the same default set as logger. |
redactErrors |
boolean |
true |
Control whether caught 5xx exception messages appear in the RFC 9457 response detail field. When true, detail is set to generic status text. When false, err.message is passed through for 5xx only — 4xx always uses generic text. Stack traces are never exposed. Only set to false in development. |
timing |
boolean | object |
false |
Inject an X-Response-Time header measuring the full request lifecycle (pipeline + error handling + send). Pass true for defaults (x-response-time header, 3 decimal places), or {header?: string, precision?: number} for custom configuration. Zero overhead when disabled. For router-level timing, see Response Timing. |
prettify |
boolean |
false |
Pretty-print JSON output (forwarded to send) |
vary |
string[] |
['Accept'] |
Vary header values to append (forwarded to send) |
etag |
boolean |
true |
Generate ETags and evaluate conditional headers (forwarded to send) |
prefer |
boolean |
false |
Read domainAcc.prefer for RFC 7240 return=minimal / return=representation (forwarded to send) |
paginate |
boolean |
false |
Read domainAcc.paginate and responseAcc.paginate to auto-generate RFC 8288 Link headers and X-Total-Count for paginated responses (forwarded to send) |
envelope |
boolean | function |
false |
Wrap 2xx Object bodies in a response envelope (forwarded to send) |
errorFormatter |
function |
— | Custom error body formatter for 4xx/5xx responses. Receives the RFC 9457 Problem Details object and {requestId, statusCode, method} context. Return value becomes the response body as application/json instead of application/problem+json. (forwarded to send) |
responseSchema |
Record<number|string, object> |
— | Map of status code to JSON Schema for response body projection (forwarded to send) |
Default Redacted Headers
Section titled “Default Redacted Headers”authorization, proxy-authorization, cookie, set-cookie
Return Value
Section titled “Return Value”Returns an async handler (req, res) => void suitable for
http.createServer().
Error Responses
Section titled “Error Responses”| Status | Condition |
|---|---|
| 500 Internal Server Error | Uncaught pipeline error (detail is redacted for security) |
Error Handling
Section titled “Error Handling”- Sets
responseAcc.statusCode = 500for uncaught pipeline errors (unless a timeout or earlier pipeline break already set the status) - Attaches
instancefrom theX-Request-Idresponse header - Emits error on
resif error listeners are present - Records the exception on the OTEL span (if tracing is configured)
- If
send()itself throws: emits the error onres(if listeners are present), records the exception on the OTEL span, and — if the response has not already been committed — setsresponseAcc.statusCode = 500and ends the response withres.statusCode = 500; res.end()
onResponse Hook
Section titled “onResponse Hook”The onResponse option registers a post-send observation callback.
It fires after the HTTP response has been written — the hook cannot
alter headers, status, or body.
responseInfo Shape
Section titled “responseInfo Shape”| Field | Type | Description |
|---|---|---|
statusCode |
number |
Final HTTP status code |
headers |
object |
Response headers snapshot (from res.getHeaders()), with sensitive headers redacted per the redactHeaders option |
method |
string |
Request HTTP method |
url |
string |
Request URL |
bodySize |
number | undefined |
Content-Length value; undefined for stream bodies |
duration |
number |
Pipeline execution time in milliseconds (from performance.now()) |
Audit Logging
Section titled “Audit Logging”import http from 'node:http';import {handler, compose} from '@centralping/ergo';
const pipeline = compose( (req, res, acc) => ({ response: {statusCode: 200, body: {ok: true}}, }),);
const server = http.createServer( handler(pipeline, { onResponse: (req, res, responseInfo, domainAcc) => { console.log(JSON.stringify({ method: responseInfo.method, url: responseInfo.url, status: responseInfo.statusCode, duration: responseInfo.duration, user: domainAcc.auth?.identity, })); }, }),);const router = createRouter({ onResponse: (req, res, responseInfo, domainAcc) => { console.log(JSON.stringify({ method: responseInfo.method, url: responseInfo.url, status: responseInfo.statusCode, duration: responseInfo.duration, user: domainAcc?.auth?.identity, })); },});Basic Usage
Section titled “Basic Usage”import http from 'node:http';import { handler, compose, logger, authorization, body,} from '@centralping/ergo';
const pipeline = compose( logger(), authorization({strategies}), body(), (req, res, acc) => ({ response: { body: processRequest(acc), statusCode: 200, }, }),);
const server = http.createServer( handler(pipeline, {debug: true, prettify: true}),);server.listen(3000);// ergo-router uses auto-wrap internally — you don't call// handler() directly. The router's handle() method creates// the two-accumulator wrapper automatically.
import createRouter, {graceful} from '@centralping/ergo-router';
const router = createRouter();router.get('/health', { execute: () => ({response: {body: {ok: true}}}),});
await graceful(router.handle(), {port: 3000});RFC References
Section titled “RFC References”Related Recipes
Section titled “Related Recipes”- Custom Middleware — Writing middleware that fits ergo’s accumulator-based return-value contract
- Debug Tracing — Finding which middleware rejected a request using pipeline debug mode
API Reference
Section titled “API Reference”See the auto-generated handler API docs.