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
FastifyAdapter.reply() performs the same consistency check as the Express adapter: if fastifyReply already has a Content-Type header that is not exactly 'application/json' while the body has statusCode >= 400, it warns and overrides the header to application/json before send(body). Note the Fastify check is exact equality, so even 'application/json; charset=utf-8' triggers it. The warning means a non-JSON content type was set on a response that ultimately delivers a JSON error body (usually the built-in exception filter's payload).
Source
Thrown at packages/platform-fastify/adapters/fastify-adapter.ts:489
fastifyReply.getHeader('Content-Disposition') === undefined &&
streamHeaders.disposition !== undefined
) {
fastifyReply.header('Content-Disposition', streamHeaders.disposition);
}
if (
fastifyReply.getHeader('Content-Length') === undefined &&
streamHeaders.length !== undefined
) {
fastifyReply.header('Content-Length', streamHeaders.length);
}
body = body.getStream();
}
if (
fastifyReply.getHeader('Content-Type') !== undefined &&
fastifyReply.getHeader('Content-Type') !== 'application/json' &&
body?.statusCode >= HttpStatus.BAD_REQUEST
) {
Logger.warn(
"Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses",
FastifyAdapter.name,
);
fastifyReply.header('Content-Type', 'application/json');
}
return fastifyReply.send(body);
}
public status(response: TRawResponse | TReply, statusCode: number) {
if (this.isNativeResponse(response)) {
response.statusCode = statusCode;
return response;
}
return (response as { code: Function }).code(statusCode);
}
public end(response: TReply, message?: string) {
response.raw.end(message!);View on GitHub (pinned to 3f8a0ce183)
Solutions
- Set Content-Type only right before sending the successful body; do risky work first.
- Custom ExceptionFilters must write the whole response themselves — reply.code(status).header('Content-Type', matching).send(body) — never set headers and delegate to the built-in filter.
- If you set 'application/json; charset=utf-8' manually, drop the charset suffix or let Fastify set JSON content type itself.
- Assert error-response Content-Type in e2e tests to catch header/error-path drift.
Example fix
// before
@Get('report')
exportReport(@Res() reply: FastifyReply) {
reply.header('Content-Type', 'text/csv');
throw new InternalServerErrorException('boom'); // JSON error body + text/csv header -> warning
}
// after
@Get('report')
async exportReport(@Res() reply: FastifyReply) {
const csv = await buildReport(); // risky work first
reply.header('Content-Type', 'text/csv');
reply.send(csv);
} Defensive patterns
Strategy: validation
Validate before calling
// Fastify: normalize the header on the error path before the built-in filter replies
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 reply = ctx.switchToHttp().getResponse();
const ct = reply.getHeader('Content-Type');
if (ct !== undefined && ct !== 'application/json') {
reply.header('Content-Type', 'application/json'); // exact match the adapter expects
}
return throwError(() => err);
}),
);
}
} Prevention
- Call reply.header('Content-Type', ...) only right before sending the success body
- Custom ExceptionFilters own the whole reply: reply.code(status).header('Content-Type', ...).send(body)
- Don't set 'application/json; charset=utf-8' manually — the adapter compares for exact 'application/json'
- Assert error-response Content-Type in e2e tests; review Fastify onSend/onResponse hooks and plugins that set default headers
When it happens
Trigger: A handler or interceptor calls reply.header('Content-Type', 'text/html') (or any non-JSON value, including 'application/json; charset=utf-8') and the request then throws an HttpException (>= 400), so the built-in exception filter's JSON error body flows through reply() with the stale header present. Also custom exception filters or Fastify hooks (onSend, onResponse) that set a Content-Type before the error body is serialized.
Common situations: File-download/streaming routes that set Content-Type before an error occurs; SSR with @Render plus JSON error paths; Fastify hooks or plugins setting default Content-Type; content-type set to a charset-suffixed JSON value, which the exact-equality check still flags; warning surfacing after a Nest upgrade that introduced the check.
Related errors
- Content-Type doesn't match Reply body, you might need a cust
- The middleware doesn't provide the 'use' method (${name})
- Conflicting HTTP routes detected: - ${messages} Adjust rou
- HTTP adapter does not support filtering on host: "${host}"
AI-assisted analysis of nestjs/nest@3f8a0ce183 (2026-08-21).
Data as JSON: /api/errors/628ccc4ec1206ef5.
Report an issue: GitHub.