Capture Request Bodies in Fastify Without Leaking Secrets

Capture Fastify request bodies for forensics with a preHandler hook that redacts passwords and tokens first, plus handling multipart and responses.

May 9, 2026·4 min read

Capture Request Bodies in Fastify Without Leaking Secrets

With SecureNow 8.11.2, automatic capture also redacts code, otpCode, oneTimeCode, verificationCode, and passcode credentials in bodies, URL queries, and captured headers. Case/separator variants are covered; status_code and error_code remain useful in traces. Keep this rule in manual capture utilities too.

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 OTP_FIELDS = new Set(['code', 'otp', 'otpcode', 'onetimecode', 'onetimepassword', 'passcode', 'verificationcode', 'confirmationcode', 'emailcode', 'smscode', 'logincode', 'signincode', 'authcode', 'authenticationcode', 'mfacode', 'totpcode', '2facode', 'securitycode', 'recoverycode', 'backupcode']);
const isOtpField = (key) => OTP_FIELDS.has(key.replace(/^x[-_]/i, '').replace(/[^a-z0-9]/gi, '').toLowerCase());

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()) || isOtpField(key)) 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') || isOtpField(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 request bodies and headers with redaction across Fastify routes (response bodies are not captured). The hand-rolled version above gives you fine control; SecureNow gives you maintenance-free.

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

Block an IP, a Range or One Route Without a Deploy (Node.js)

Block one IP, a CIDR range, or a single route and method in a Node.js app within seconds, with no code change: setup, scoping, CLI, MCP, verification and undo.

Oct 27
11 Signs Your AI-Built App Needs a Security Audit

Built your app with a coding agent? 11 warning signs it needs a security audit, each checkable in five minutes, and how to run the full audit yourself.

Oct 26
AI Security Audit Part 1: Set Up Your Agent to Audit Code

Part 1 of the AI security audit series: set up an AI coding agent to audit your app locally, connect SecureNow, and run your first threat-model pass.

Jul 24