Capturing Request Bodies for Forensics in Nuxt Without Leaking Secrets
Nuxt server middleware + h3 utilities give you a clean hook for body capture. Here's the redaction-aware pattern that works on Nuxt 3 and self-hosted Nitro.
Capturing Request Bodies for Forensics in Nuxt Without Leaking Secrets
Nuxt 3 server routes (in server/api/) get the body via readBody(event). Add a server middleware that runs before route handlers to capture it once.
The redaction utility
// server/utils/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',
]);
export function redact(obj: any, depth = 0): any {
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: 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;
}
The middleware
// server/middleware/forensics.ts
import { trace } from '@opentelemetry/api';
import { redact } from '../utils/redact';
export default defineEventHandler(async (event) => {
const path = getRequestURL(event).pathname;
if (!path.startsWith('/api/')) return;
// Read body — must use a non-destructive method since route handler will also call readBody
const body = await readBody(event).catch(() => null);
const headers = getHeaders(event);
const span = trace.getActiveSpan();
if (span) {
if (body) {
const safe = JSON.stringify(redact(body)).slice(0, 8192);
span.setAttribute('http.request.body', safe);
}
span.setAttribute('http.request.headers', JSON.stringify(redactHeaders(headers)));
}
});
function redactHeaders(headers: Record<string, string | undefined>) {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
if (!value) continue;
const lower = key.toLowerCase();
result[key] = (lower === 'authorization' || lower === 'cookie' || lower.startsWith('x-api-key'))
? '[REDACTED]'
: String(value);
}
return result;
}
Caveat: body consumption
readBody() consumes the request stream. If the middleware reads it, the route handler can't read it again. Two workarounds:
- Tee the body: read it in middleware, attach to event context, route handler reads from context.
event.context.cachedBody = await readBody(event);
In your route handler:
const body = event.context.cachedBody ?? await readBody(event);
- Use the SecureNow preload, which handles this transparently.
Response capture
// server/middleware/forensics-response.ts
export default defineEventHandler((event) => {
event.node.res.on('finish', () => {
// Capture response metadata only; body is harder to intercept
const span = trace.getActiveSpan();
span?.setAttribute('http.response.status', event.node.res.statusCode);
});
});
For full response body capture, use the SecureNow auto-instrumentation — manual response interception in Nitro is fiddly.
The automatic option
node -r securenow/register .output/server/index.mjs
Auto-captures bodies, headers, and response data with redaction across all Nuxt server routes. No middleware needed.
Related
Frequently Asked Questions
Does this work in Nuxt server routes (server/api)?
Yes. Capture happens in middleware that runs before the route handler, or inside the route handler using `readBody()`.
What about for Vercel-deployed Nuxt?
Same code works. The redaction logic doesn't depend on the runtime.
Is there an automatic option?
Yes — preload `securenow/register` and bodies are auto-captured with redaction across all Nuxt server routes.
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