Capturing Request Bodies for Forensics in Fastify Without Leaking Secrets
A Fastify preHandler hook captures bodies for forensics. Here's the redaction-aware version that won't put passwords in your logs.
Capturing Request Bodies for Forensics in Fastify Without Leaking Secrets
Fastify's preHandler hook is the place. Bodies are parsed, route is matched, and you can attach the redacted version to your trace span before the handler runs.
The hook
import { trace } from '@opentelemetry/api';
const SENSITIVE = new Set([
'password', 'pass', 'pwd', 'token', 'access_token', 'refresh_token',
'api_key', 'apikey', 'secret', 'authorization', 'cookie', 'session',
'creditcard', 'card_number', 'cvv', 'cvc', 'ssn',
]);
function redact(obj, depth = 0) {
if (depth > 10) return '[max-depth]';
if (obj == null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map(item => redact(item, depth + 1));
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE.has(key.toLowerCase())) result[key] = '[REDACTED]';
else if (typeof value === 'object') result[key] = redact(value, depth + 1);
else result[key] = value;
}
return result;
}
export async function forensicsHook(req, reply) {
const span = trace.getActiveSpan();
if (!span) return;
if (req.body && typeof req.body === 'object') {
const safe = JSON.stringify(redact(req.body));
span.setAttribute('http.request.body', safe.slice(0, 8192));
}
span.setAttribute('http.request.headers', JSON.stringify(redactHeaders(req.headers)));
}
function redactHeaders(headers) {
const result = {};
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
result[key] = (lower === 'authorization' || lower === 'cookie' || lower.startsWith('x-api-key'))
? '[REDACTED]'
: String(value);
}
return result;
}
Wiring globally
import Fastify from 'fastify';
import { forensicsHook } from './forensics.js';
const app = Fastify({ trustProxy: 1 });
app.addHook('preHandler', forensicsHook);
Multipart bodies
Skip file content; capture only field metadata:
async function safeMultipart(req) {
if (!req.isMultipart?.()) return null;
const fields = {};
for await (const part of req.parts()) {
if (part.type === 'field') {
fields[part.fieldname] = SENSITIVE.has(part.fieldname.toLowerCase())
? '[REDACTED]'
: part.value;
} else {
fields[part.fieldname] = `[file: ${part.filename}, ${part.mimetype}]`;
}
}
return fields;
}
Response capture
app.addHook('onSend', async (req, reply, payload) => {
const span = trace.getActiveSpan();
if (span && payload && reply.getHeader('content-type')?.includes('json')) {
try {
const parsed = JSON.parse(payload);
span.setAttribute('http.response.body', JSON.stringify(redact(parsed)).slice(0, 8192));
} catch {}
}
return payload;
});
The automatic option
node -r securenow/register server.js
Auto-captures bodies, headers, response payloads with redaction across Fastify routes. The hand-rolled version above gives you fine control; SecureNow gives you maintenance-free.
Related
Frequently Asked Questions
Why preHandler instead of onRequest?
preHandler runs after body parsing, so `req.body` is populated. onRequest runs before parsing — no body to capture.
Does this work with all body types (JSON, multipart, etc.)?
JSON yes, multipart needs special handling because the file streams shouldn't be captured (huge). Multipart bodies should capture only the field metadata, not file contents.
Is there an automatic option?
Yes — preload `securenow/register` and the SDK auto-captures bodies for Fastify with redaction across all routes.
Recommended reading
If your team uses Sentry for frontend errors and needs backend distributed tracing without doubling the Sentry bill, here's the OpenTelemetry path that doesn't make you choose.
May 9Five approaches to bot blocking in Express, ranked by effort vs. effectiveness. From a 5-line allowlist to a full IP-reputation firewall — all without Cloudflare, AWS WAF, or any new infrastructure.
May 9Fastify hooks (onRequest) and the SecureNow preload both work cleanly. Here's the production setup for IP blocking and user-agent filtering.
May 9