Capture Request Bodies in Nuxt Without Leaking Secrets
Capture Nuxt request bodies for forensics with server middleware and h3 utilities, redacting secrets first. Works on Nuxt 3 and self-hosted Nitro.
Capture Request Bodies in Nuxt Without Leaking Secrets
With SecureNow 8.11.2, automatic capture also redacts code, otpCode, oneTimeCode, verificationCode, and passcode credentials in bodies, URL queries, and captured headers. Case/separator variants are covered; status_code and error_code remain useful in traces. Keep this rule in manual capture utilities too.
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 OTP_FIELDS = new Set(['code', 'otp', 'otpcode', 'onetimecode', 'onetimepassword', 'passcode', 'verificationcode', 'confirmationcode', 'emailcode', 'smscode', 'logincode', 'signincode', 'authcode', 'authenticationcode', 'mfacode', 'totpcode', '2facode', 'securitycode', 'recoverycode', 'backupcode']);
const isOtpField = (key) => OTP_FIELDS.has(key.replace(/^x[-_]/i, '').replace(/[^a-z0-9]/gi, '').toLowerCase());
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()) || isOtpField(key)) 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') || isOtpField(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);
});
});
The SecureNow auto-instrumentation records the response status, not the response body — and manual response body interception in Nitro is fiddly.
The automatic option
node -r securenow/register .output/server/index.mjs
Auto-captures request bodies and headers, plus the response status, 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
Block one IP, a CIDR range, or a single route and method in a Node.js app within seconds, with no code change: setup, scoping, CLI, MCP, verification and undo.
Oct 27Built your app with a coding agent? 11 warning signs it needs a security audit, each checkable in five minutes, and how to run the full audit yourself.
Oct 26Part 1 of the AI security audit series: set up an AI coding agent to audit your app locally, connect SecureNow, and run your first threat-model pass.
Jul 24