Capturing Request Bodies for Forensics in NestJS Without Leaking Secrets

NestJS interceptors give you a clean hook for body capture. Here's the redaction-aware pattern that works across REST, GraphQL, and microservices.

May 9, 2026·5 min read

Capturing Request Bodies for Forensics in NestJS Without Leaking Secrets

NestJS Interceptors are the right hook for body capture. They have full ExecutionContext access, run after Guards, and can wrap handler invocation. Here's the pattern.

The redaction utility

// src/forensics/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 || 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;
}

The Interceptor

// src/forensics/forensics.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
import { trace } from '@opentelemetry/api';
import { redact } from './redact';

@Injectable()
export class ForensicsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    const span = trace.getActiveSpan();

    if (span && req.body && Object.keys(req.body).length > 0) {
      span.setAttribute('http.request.body', JSON.stringify(redact(req.body)));
    }
    if (span && req.headers) {
      span.setAttribute('http.request.headers', JSON.stringify(redactHeaders(req.headers)));
    }

    return next.handle().pipe(
      tap((responseBody) => {
        if (span && responseBody) {
          span.setAttribute('http.response.body', JSON.stringify(redact(responseBody)));
        }
      })
    );
  }
}

function redactHeaders(headers: Record<string, any>) {
  const result: Record<string, string> = {};
  for (const [key, value] of Object.entries(headers)) {
    const lower = key.toLowerCase();
    result[key] = (lower === 'authorization' || lower === 'cookie' || lower.startsWith('x-api-key'))
      ? '[REDACTED]'
      : String(value);
  }
  return result;
}

Wiring globally

// app.module.ts
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ForensicsInterceptor } from './forensics/forensics.interceptor';

@Module({
  providers: [
    { provide: APP_INTERCEPTOR, useClass: ForensicsInterceptor },
  ],
})
export class AppModule {}

Now every controller route captures redacted bodies and headers automatically.

Per-controller / per-route

If global is too broad:

import { UseInterceptors } from '@nestjs/common';
import { ForensicsInterceptor } from './forensics/forensics.interceptor';

@Controller('users')
@UseInterceptors(ForensicsInterceptor)
export class UsersController {
  // ...
}

GraphQL

For GraphQL resolvers, the body is the variables object. The Interceptor pattern is identical, but you read from the GraphQL execution context:

import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class GraphQLForensicsInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const ctx = GqlExecutionContext.create(context);
    const args = ctx.getArgs();
    const span = trace.getActiveSpan();
    span?.setAttribute('graphql.input', JSON.stringify(redact(args)));
    return next.handle();
  }
}

Microservices

For gRPC / RMQ / NATS handlers:

const data = context.switchToRpc().getData();
span?.setAttribute('rpc.payload', JSON.stringify(redact(data)));

Same redaction, different context type.

Size limits

Cap captured body size:

function safeStringify(obj: unknown, maxBytes = 8192): string {
  const str = JSON.stringify(redact(obj));
  return str.length > maxBytes ? str.slice(0, maxBytes) + '…[truncated]' : str;
}

The automatic option

node -r securenow/register dist/main.js

Auto-captures bodies, headers, and response payloads across REST, GraphQL, and microservice handlers — with redaction built in. The interceptor pattern above is what you'd reach for if you want fine-grained control or are not using SecureNow.

Related

Frequently Asked Questions

Should I use a Pipe, Interceptor, or Middleware?

Interceptor. Pipes transform values for handlers; Middleware runs before guards but has limited handler context; Interceptors run with full handler metadata, which is what you want for body capture.

Does this work for GraphQL?

Yes — NestJS Interceptors work for GraphQL resolvers via `@UseInterceptors()` on the resolver class. The body is the GraphQL variables object.

What about microservices (gRPC, NATS, RMQ)?

Same Interceptor pattern; the body is the message payload. Just capture in the appropriate `RpcArgumentsHost` instead of `HttpArgumentsHost`.

Is there an automatic option?

Yes — preload `securenow/register` and the SDK auto-captures bodies across all NestJS controllers, GraphQL resolvers, and microservice handlers, with redaction built in.

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