{"record":{"id":"628ccc4ec1206ef5","repo":"nestjs/nest","slug":"content-type-doesn-t-match-reply-body-you-might-n-628ccc","errorCode":null,"errorMessage":"Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses","messagePattern":"Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/platform-fastify/adapters/fastify-adapter.ts","lineNumber":489,"sourceCode":"        fastifyReply.getHeader('Content-Disposition') === undefined &&\n        streamHeaders.disposition !== undefined\n      ) {\n        fastifyReply.header('Content-Disposition', streamHeaders.disposition);\n      }\n      if (\n        fastifyReply.getHeader('Content-Length') === undefined &&\n        streamHeaders.length !== undefined\n      ) {\n        fastifyReply.header('Content-Length', streamHeaders.length);\n      }\n      body = body.getStream();\n    }\n    if (\n      fastifyReply.getHeader('Content-Type') !== undefined &&\n      fastifyReply.getHeader('Content-Type') !== 'application/json' &&\n      body?.statusCode >= HttpStatus.BAD_REQUEST\n    ) {\n      Logger.warn(\n        \"Content-Type doesn't match Reply body, you might need a custom ExceptionFilter for non-JSON responses\",\n        FastifyAdapter.name,\n      );\n      fastifyReply.header('Content-Type', 'application/json');\n    }\n    return fastifyReply.send(body);\n  }\n\n  public status(response: TRawResponse | TReply, statusCode: number) {\n    if (this.isNativeResponse(response)) {\n      response.statusCode = statusCode;\n      return response;\n    }\n    return (response as { code: Function }).code(statusCode);\n  }\n\n  public end(response: TReply, message?: string) {\n    response.raw.end(message!);","sourceCodeStart":471,"sourceCodeEnd":507,"githubUrl":"https://github.com/nestjs/nest/blob/3f8a0ce1832fb30053f55856088371b097bbf7e1/packages/platform-fastify/adapters/fastify-adapter.ts#L471-L507","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n@Get('report')\nexportReport(@Res() reply: FastifyReply) {\n  reply.header('Content-Type', 'text/csv');\n  throw new InternalServerErrorException('boom'); // JSON error body + text/csv header -> warning\n}\n\n// after\n@Get('report')\nasync exportReport(@Res() reply: FastifyReply) {\n  const csv = await buildReport(); // risky work first\n  reply.header('Content-Type', 'text/csv');\n  reply.send(csv);\n}","handlingStrategy":"validation","validationCode":"// Fastify: normalize the header on the error path before the built-in filter replies\nimport { catchError, throwError, CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';\n\n@Injectable()\nexport class JsonErrorHeaderInterceptor implements NestInterceptor {\n  intercept(ctx: ExecutionContext, next: CallHandler) {\n    return next.handle().pipe(\n      catchError((err) => {\n        const reply = ctx.switchToHttp().getResponse();\n        const ct = reply.getHeader('Content-Type');\n        if (ct !== undefined && ct !== 'application/json') {\n          reply.header('Content-Type', 'application/json'); // exact match the adapter expects\n        }\n        return throwError(() => err);\n      }),\n    );\n  }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["fastify","http","content-type","exception-filter","http-errors"],"backgroundTag":"content-type-mismatch-error-response","analyzedSha":"3f8a0ce1832fb30053f55856088371b097bbf7e1","analyzedAt":"2026-08-21T19:39:39.867Z","contentChangedAt":"2026-08-21T19:39:39.867Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}