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

Create Custom Alert Rules From the Command Line

SecureNow 8.1 adds `securenow alerts rules create` — define your own detection rules from SQL, scope them to your apps, and ship them without leaving the terminal. Here's how, with a real magic-link brute-force example.

Jun 11
Secure a Next.js App with SecureNow Using This AI Onboarding Prompt

A copy-paste prompt that lets an AI coding agent install SecureNow, wire Next.js instrumentation, verify traces and logs, deploy to AWS, simulate attacks, and prove firewall blocking with human approval gates.

May 18
nextjs securenow ai onboarding prompt
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