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.
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
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