Add Headers, Wrap Responses, Sign Cookies
onSend Hook — Mutate the Outgoing Payload
onSend runs after serialization, just before bytes hit the socket — the right place for response wrappers, header injection, and signing.
What you'll learn
- Use onSend to inject headers like X-Trace-Id
- Wrap successful payloads in an envelope
- Avoid heavy CPU work in onSend
onSend is the last hook before Fastify writes the response. The serialized payload is already
prepared; you can swap it, append headers, or audit what’s leaving the server.
Add a Trace Header
app.addHook('onSend', async (req, reply, payload) => {
reply.header('X-Trace-Id', req.id);
return payload; // return the same payload to leave it untouched
}); Return the original payload (a string, Buffer, or stream) to leave the body unchanged.
Return a new value to replace it.
Envelope Successful Responses
app.addHook('onSend', async (req, reply, payload) => {
if (reply.statusCode >= 200 && reply.statusCode < 300 && typeof payload === 'string') {
const body = JSON.parse(payload);
return JSON.stringify({ ok: true, data: body, requestId: req.id });
}
return payload;
}); If you do this, declare a response schema that matches the envelope — otherwise fast-json-stringify
won’t help and the parse/stringify round-trip is wasted work.
Be Careful With Streams
When payload is a stream, you cannot inspect it without consuming it. Either skip stream
responses with an early return or pipe through a transform that re-emits.
Don’t Block
onSend runs synchronously on the request hot path. Anything CPU-heavy (compression, big-payload
JSON manipulation) belongs in middleware that streams the work, or in a worker. Cookie signing
and small header additions are fine.
Pair With onResponse
onSend modifies; onResponse fires after the response is fully written (great for metrics).
Use them together — one to shape, one to observe.