Capturing Request Bodies for Forensics in Next.js Without Leaking Secrets

Next.js Route Handlers and Server Actions need redaction-aware body capture. Here's the pattern that works in App Router.

Lhoussine
May 9, 2026·6 min read

Capturing Request Bodies for Forensics in Next.js Without Leaking Secrets

For incident investigation in Next.js, request bodies in your traces are the difference between "something failed" and "we know exactly what they sent." The bad version logs everything including passwords; the good version redacts before storing.

Here's the App Router pattern. For broader context see the application security monitoring guide.

The redaction utility

Same pattern as Express, packaged for reuse:

// lib/redact.ts
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', 'tax_id',
]);

export function redact<T>(obj: T, depth = 0): T {
  if (depth > 10) return '[max-depth]' as any;
  if (obj === null || obj === undefined) return obj;
  if (typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) return obj.map(item => redact(item, depth + 1)) as any;

  const result: any = {};
  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;
}

Route Handlers (App Router)

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { trace } from '@opentelemetry/api';
import { redact } from '@/lib/redact';

export async function POST(req: NextRequest) {
  const body = await req.json();

  const span = trace.getActiveSpan();
  span?.setAttribute('http.request.body', JSON.stringify(redact(body)));

  // ... your handler logic
  const user = await createUser(body);
  return NextResponse.json({ user });
}

The trace.getActiveSpan() requires OpenTelemetry instrumentation set up via instrumentation.ts. If you're not yet on OTel, you can instead log the redacted body directly:

console.log(JSON.stringify({
  type: 'request',
  path: '/api/users',
  body: redact(body),
}));

Server Actions

Server Actions in App Router get the body as the function arguments:

// app/actions/create-order.ts
'use server';

import { trace } from '@opentelemetry/api';
import { redact } from '@/lib/redact';

export async function createOrder(formData: FormData) {
  const data = Object.fromEntries(formData);

  const span = trace.getActiveSpan();
  span?.setAttribute('app.action.input', JSON.stringify(redact(data)));

  // ... action logic
}

Same pattern: redact, attach to active span, then process.

Pages Router (legacy)

For projects still on Pages Router:

// pages/api/users.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { trace } from '@opentelemetry/api';
import { redact } from '@/lib/redact';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const span = trace.getActiveSpan();
  if (req.body) {
    span?.setAttribute('http.request.body', JSON.stringify(redact(req.body)));
  }
  // ... handler
}

Headers and query params

Request bodies aren't the only place secrets leak. Headers (Authorization, Cookie) and query strings (?api_key=...) need the same treatment:

function redactHeaders(headers: Headers) {
  const result: Record<string, string> = {};
  headers.forEach((value, key) => {
    const lower = key.toLowerCase();
    result[key] = (lower === 'authorization' || lower === 'cookie' || lower.startsWith('x-api-key'))
      ? '[REDACTED]'
      : value;
  });
  return result;
}

span?.setAttribute('http.request.headers', JSON.stringify(redactHeaders(req.headers)));

Size limits

Bodies > 8KB are rare for legitimate requests. Cap captured size to keep storage bounded:

function safeStringify(obj: unknown, maxBytes = 8192) {
  const str = JSON.stringify(redact(obj));
  return str.length > maxBytes
    ? str.slice(0, maxBytes) + `…[truncated, ${str.length} bytes]`
    : str;
}

The automatic option

Hand-rolling this in every route handler is repetitive. The SecureNow npm package wraps OpenTelemetry with auto-capture and redaction:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    await import('securenow/register');
  }
}

That's the entire setup. Bodies are captured automatically across all Route Handlers, API routes, and Server Actions. The redaction list is configurable. Free tier includes the body capture.

For Edge Runtime portions of your app (middleware, Edge Route Handlers), the auto-capture doesn't run — Edge Runtime restrictions prevent the heavy instrumentation. For those, hand-roll using the patterns above.

Verifying redaction

Test it:

const test = {
  email: 'user@example.com',
  password: 'should-not-leak',
  metadata: { token: 'should-also-not-leak', name: 'fine to log' },
};
console.log(redact(test));
// {
//   email: 'user@example.com',
//   password: '[REDACTED]',
//   metadata: { token: '[REDACTED]', name: 'fine to log' }
// }

If anything sensitive comes through unredacted, add the field name to SENSITIVE and re-test.

Related

Frequently Asked Questions

Where do I capture request bodies in Next.js App Router?

Inside Route Handlers (`route.ts` files) — that's where you have direct access to the Request object. For Server Actions, capture happens in the action function itself.

Can I capture bodies via middleware?

Not directly. Next.js middleware runs in Edge Runtime and doesn't allow consuming the body without breaking downstream handlers. Capture in the route handler instead.

What about getStaticProps / getServerSideProps?

Pages Router patterns. Same redaction logic applies; capture in the handler before processing.

Is there an automatic option?

Yes — preload `securenow/register` (Node runtime) and the SDK auto-captures bodies with redaction across Route Handlers, API routes, and Server Actions.

Recommended reading

Adding Backend Tracing to a Sentry Stack with OpenTelemetry

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 9
How to Block Bot Traffic in Express With No Extra Infra

Five 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 9
How to Block Bot Traffic in Fastify With No Extra Infra

Fastify hooks (onRequest) and the SecureNow preload both work cleanly. Here's the production setup for IP blocking and user-agent filtering.

May 9