nestjs/nest · warning

Content-Type doesn't match Reply body, you might need a cust

Error message

Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses

What it means

ExpressAdapter.reply() runs for bodies Nest sends through the adapter, including the built-in exception filter's {statusCode, message, error} payload. If the response already has a string Content-Type header that is not application/json while the body is an object with statusCode >= 400, the adapter warns and force-sets Content-Type to application/json before JSON-serializing with response.json(). The warning flags the inconsistency: something set a non-JSON content type on a response that ends up carrying a JSON error body.

Source

Thrown at packages/platform-express/adapters/express-adapter.ts:131

      return response.send();
    }
    if (body instanceof StreamableFile) {
      this.applyStreamHeaders(response, body);
      const stream = body.getStream();
      stream.once('error', err => {
        body.errorHandler(err, response);
      });
      return stream
        .pipe<Writable>(response)
        .on('error', (err: Error) => body.errorLogger(err));
    }
    const responseContentType = response.getHeader('Content-Type');
    if (
      typeof responseContentType === 'string' &&
      !responseContentType.startsWith('application/json') &&
      body?.statusCode >= HttpStatus.BAD_REQUEST
    ) {
      this.logger.warn(
        "Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses",
      );
      response.setHeader('Content-Type', 'application/json');
    }
    return isObject(body) ? response.json(body) : response.send(String(body));
  }

  public status(response: any, statusCode: number) {
    return response.status(statusCode);
  }

  public end(response: any, message?: string) {
    return response.end(message);
  }

  public render(response: any, view: string, options: any) {
    return response.render(view, options);
  }

View on GitHub (pinned to dd75d7bd8c)

Solutions

  1. Defer setting Content-Type until after the failure-prone work: generate the CSV/file first, then set headers and send.
  2. Make custom ExceptionFilters fully own the response: set status + matching Content-Type and write the body (res.status(...).send/render/json) rather than setting headers and letting the built-in filter serialize.
  3. Scope interceptors that set Content-Type away from routes whose error responses are JSON, or reset the header on the error path.
  4. Add e2e assertions that error responses keep an application/json Content-Type to catch regressions.

Example fix

// before
@Get('report')
exportReport(@Res() res: Response) {
  res.setHeader('Content-Type', 'text/csv');
  throw new InternalServerErrorException('boom'); // JSON error body + text/csv header -> warning
}

// after
@Get('report')
async exportReport(@Res() res: Response) {
  const csv = await buildReport(); // risky work first
  res.setHeader('Content-Type', 'text/csv');
  res.send(csv);
}
Defensive patterns

Strategy: validation

Validate before calling

// keep the header consistent with the built-in filter's JSON error body
import { catchError, throwError, CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';

@Injectable()
export class JsonErrorHeaderInterceptor implements NestInterceptor {
  intercept(ctx: ExecutionContext, next: CallHandler) {
    return next.handle().pipe(
      catchError((err) => {
        const res = ctx.switchToHttp().getResponse();
        if (!res.headersSent && res.getHeader('Content-Type')) {
          res.setHeader('Content-Type', 'application/json');
        }
        return throwError(() => err);
      }),
    );
  }
}

Prevention

When it happens

Trigger: A handler or interceptor calls res.setHeader('Content-Type', 'text/html' / 'text/csv' / ...) and the request then fails with an HttpException (>= 400), so the built-in exception filter writes its JSON error body through reply() while the stale non-JSON header is still set. Also custom exception filters that set a non-JSON Content-Type but delegate the actual body to the framework instead of writing it themselves.

Common situations: CSV/file-download endpoints that set headers before generating the file, then throw; SSR or hybrid apps mixing HTML templates with JSON API errors; middleware or template engines setting Content-Type early; the warning appearing after upgrading Nest to a release that added this consistency check.

Related errors


AI-assisted analysis of nestjs/nest@dd75d7bd8c (2026-08-21). Data as JSON: /api/errors/a65dacc360421926. Report an issue: GitHub.