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.

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

AI Security Auditing, Part 1: Set Up Your AI Agent to Audit Your Code

Part 1 of the AI Security Auditing series. Set up an AI coding agent to audit your application's security locally — install SecureNow, connect your account, and run your first threat-model pass in minutes.

Jul 24
How to Audit Your App's Security With AI: The 2026 Guide

A practical, end-to-end guide to auditing your application's security with an AI coding agent — find vulnerabilities at the code level, build detections at the runtime level, and never upload your codebase to do it.

Jul 24
AI Security Auditing, Part 2: Audit Authentication & Sessions With AI

Part 2 of the AI Security Auditing series. Use an AI agent to audit your login, session, MFA, password-reset and magic-link flows for account-takeover paths — then catch the attempts in live traffic.

Jul 23