NestJS TypeORM报错Cannot set headers after they are sent
NestJS GET接口返回正常但进程崩溃问题排查
问题现象
开发NestJS应用的GET类型REST API时,调用接口可以正常返回预期的空数据{"wes":[]}(数据库无数据),但Node.js应用会直接崩溃,抛出ERR_HTTP_HEADERS_SENT错误,错误核心信息为:
Cannot set headers after they are sent to the client
相关代码
TypeORM实体类定义
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; @Entity({name: 'workingEnvs'}) export class WorkingEnvironment { @PrimaryColumn({ name: 'id', type: 'varchar', length: 32, default: "defaultId", }) id: string; @Column({ name: 'name', type: 'varchar', length: 32, default: "workingEnv-420", }) name: string; @Column({ name: 'something', type: 'varchar', length: 32, default: "practice", }) something: string; @Column({ default: true}) isAvailable: boolean; }
控制器路由代码
@Get() async getCollection(@Res() response) { const wes = await this.weService.findAll(); return response.status(HttpStatus.OK).json({ wes, }); }
服务层业务代码
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from "typeorm"; import { WorkingEnvironment } from './workingenv.entity'; @Injectable() export class WorkingEnvService { constructor( @InjectRepository(WorkingEnvironment) private weRepository: Repository<WorkingEnvironment> ) {} // GET collection service async findAll(): Promise<WorkingEnvironment[]> { return await this.weRepository.find(); } }
崩溃错误栈
node:internal/errors:463 ErrorCaptureStackTrace(err); ^ Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client at new NodeError (node:internal/errors:370:5) at ServerResponse.setHeader (node:_http_outgoing:573:11) at ServerResponse.header (node_modules\express\lib\response.js:771:10) at ServerResponse.send (node_modules\express\lib\response.js:170:12) at ServerResponse.json (node_modules\express\lib\response.js:267:15) at ExpressAdapter.reply (node_modules\@nestjs\platform-express\adapters\express-adapter.js:24:57) at ExceptionsHandler.handleUnknownError (node_modules\@nestjs\core\exceptions\base-exception-filter.js:33:24) at ExceptionsHandler.catch (node_modules\@nestjs\core\exceptions\base-exception-filter.js:17:25) at ExceptionsHandler.next (node_modules\@nestjs\core\exceptions\exceptions-handler.js:16:20) at node_modules\@nestjs\core\router\router-proxy.js:13:35 { code: 'ERR_HTTP_HEADERS_SENT' }
补充配置
app.module.ts核心配置
TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3307, username: 'root', password: 'root', database: 'workingenv', entities: [WorkingEnvironment], }), WorkingEnvModule, ], controllers: [PortableController], providers: [PortableService, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor, }],
全局LoggingInterceptor实现
import { constants, utils } from "@app/common"; import { CallHandler, ExecutionContext, HttpException, Injectable, NestInterceptor } from "@nestjs/common"; import { Observable } from "rxjs"; import { catchError, tap } from 'rxjs/operators'; import { LoggerService } from "./logger.service"; const _ = require('lodash') @Injectable() export class LoggingInterceptor implements NestInterceptor { constructor(private logger: LoggerService) { } now() { const ts = process.hrtime() return (ts[0] * 1e3) + (ts[1] / 1e6) } log(response: any, context: ExecutionContext, startTime: number, isError: Boolean = false) { const req = context.switchToHttp().getRequest(); const { method, url, body } = req; let statusCode = context.switchToHttp().getResponse().statusCode if(isError && response instanceof HttpException) { statusCode = response.getStatus(); } const endTime = this.now() if(!_.includes(url, 'health')) { const requestId = req.headers[constants.requestIdHeaderName] this.logger.access( `\t${requestId}\t${statusCode}\t${method}\t${url}\t${endTime - startTime}\t${req.user?.sub}\t${JSON.stringify(utils.scrub(body))}\t${JSON.stringify(utils.scrub(response))}`, ); } } public intercept(context: ExecutionContext, call$: CallHandler): Observable<unknown> { const startTime = this.now() return call$.handle().pipe( tap({ next: (val: unknown): void => { this.log(val, context, startTime); }, error: (err: Error): void => { this.log(err, context, startTime, true); }, }), catchError(err => { throw err }) ); } }
根因分析
问题由两处代码不规范共同导致:
- 控制器手动响应后返回了Response对象:控制器中调用
response.status().json({wes})时,已经第一次将响应头、响应体发送给客户端,客户端能正常拿到返回值。但response.json()的返回值是Express的Response实例本身,代码将这个实例作为控制器方法的返回值交回给NestJS处理流。 - 拦截器序列化Response实例触发异常:全局日志拦截器拿到控制器返回的Response实例后,在log方法中直接对该对象执行
JSON.stringify操作。Response实例本身存在循环引用,序列化时会直接抛出错误,错误被rxjs捕获重新抛给NestJS异常过滤器。 - 异常过滤器二次响应触发崩溃:NestJS异常过滤器尝试向客户端发送错误响应时,发现响应已经在第一步发送完成,无法再设置HTTP头,直接抛出
ERR_HTTP_HEADERS_SENT错误导致进程崩溃。
注:实体类中冗余导入PrimaryGeneratedColumn不影响本次运行结果,属于无用代码。
修复方案
优先采用NestJS推荐的标准响应处理方式,避免手动操作Response对象:
- 移除控制器中的
@Res()注入,直接返回业务数据,NestJS会自动处理200状态码和JSON序列化,修改后控制器代码:
@Get() async getCollection() { const wes = await this.weService.findAll(); return { wes }; }
- (可选加固)在日志拦截器的序列化逻辑中增加兜底判断,对Request/Response这类存在循环引用的系统对象做特殊处理,避免序列化失败抛出未捕获异常。
如果确实需要手动操作Response对象,注入时需要增加passthrough: true配置告知NestJS不要完全接管响应流,且手动调用json()/send()方法后不要再返回Response实例。
内容的提问来源于stack exchange,提问作者Pradip
相关产品推荐
相关产品推荐

