You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
            })
          );
    }
}

根因分析

问题由两处代码不规范共同导致:

  1. 控制器手动响应后返回了Response对象:控制器中调用response.status().json({wes})时,已经第一次将响应头、响应体发送给客户端,客户端能正常拿到返回值。但response.json()的返回值是Express的Response实例本身,代码将这个实例作为控制器方法的返回值交回给NestJS处理流。
  2. 拦截器序列化Response实例触发异常:全局日志拦截器拿到控制器返回的Response实例后,在log方法中直接对该对象执行JSON.stringify操作。Response实例本身存在循环引用,序列化时会直接抛出错误,错误被rxjs捕获重新抛给NestJS异常过滤器。
  3. 异常过滤器二次响应触发崩溃:NestJS异常过滤器尝试向客户端发送错误响应时,发现响应已经在第一步发送完成,无法再设置HTTP头,直接抛出ERR_HTTP_HEADERS_SENT错误导致进程崩溃。

注:实体类中冗余导入PrimaryGeneratedColumn不影响本次运行结果,属于无用代码。

修复方案

优先采用NestJS推荐的标准响应处理方式,避免手动操作Response对象:

  1. 移除控制器中的@Res()注入,直接返回业务数据,NestJS会自动处理200状态码和JSON序列化,修改后控制器代码:
@Get()
async getCollection() {
    const wes = await this.weService.findAll();
    return { wes };
}
  1. (可选加固)在日志拦截器的序列化逻辑中增加兜底判断,对Request/Response这类存在循环引用的系统对象做特殊处理,避免序列化失败抛出未捕获异常。

如果确实需要手动操作Response对象,注入时需要增加passthrough: true配置告知NestJS不要完全接管响应流,且手动调用json()/send()方法后不要再返回Response实例。

内容的提问来源于stack exchange,提问作者Pradip

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 13:09:28