Express에서 ‘Can’t set headers after they are sent to the client’ 오류 해결하기

1. 문제 정의

Node.js + Express 로 Facebook 인증처럼 콜백이 여러 번 이어지는 라우트를 구현하다 보면 다음과 같은 에러를 만납니다.

1
2
3
4
5
Error: Can't render headers after they are sent to the client.
    at ServerResponse.<anonymous> (http.js:573:11)
    at ServerResponse._renderHeaders (.../connect/lib/patch.js:64:25)
    at ServerResponse.writeHead (http.js:813:20)
    at .../connect-auth/lib/auth.strategies/facebook.js:28:15

즉, 이미 응답이 클라이언트에게 나간 뒤에 헤더를 다시 설정하려고 시도해서 생기는 런타임 오류입니다. 이 StackOverflow 질문은 1240점으로 매우 널리 알려진 Express 오류입니다.

2. 원인 탐구

Express의 res 객체는 Node.js의 http.ServerResponse를 상속합니다. 서버가 응답을 구성하는 상태 흐름은 다음과 같습니다.

  1. Headres.setHeader(), res.statusCode 설정 가능
  2. Bodyres.write(data)로 본문 전송 시작
  3. Finishedres.end() 후 종료

res.setHeader()res.writeHead()를 호출하기 전까지는 몇 번이든 호출할 수 있지만, writeHead 이후에는 헤더가 확정되므로 본문을 쓰거나 end 한 뒤에는 상태나 헤더를 더 이상 바꿀 수 없습니다.

3. 근본 원인 분석

에러 메시지가 나타나는 근본 원인은 **‘Body 또는 Finished 상태에서 어떤 함수가 헤더/statusCode를 다시 설정하려는 것’**입니다.

대표적인 트리거:

  • 중복 응답: 어떤 경로에서 res.send()/res.end()가 두 번 호출.
  • redirect 후 추가 처리: 원 질문의 경우 res.redirect()로 응답이 Finished가 된 다음, 이후 코드에서 예외(res.req가 null)가 발생.
  • Connect가 그 예외를 잡아 500 페이지를 다시 전송하려 하는데, 헤더가 이미 나갔으므로 setHeader가 실패.

즉, 단순히 “한 줄 고치기” 보다 응답이 정확히 한 번만 완료되도록 라우트를 분석해야 합니다.

4. 코드 해결책

반드시 지켜야 할 규칙

1
2
3
4
5
// ✅ 헤더를 먼저 설정
res.status(200);
res.setHeader('Content-Type', 'application/json');
// 그 다음 본문 전송
res.json({ ok: true });
1
2
3
// ❌ 본문 이후 헤더 재설정 → 에러 발생
res.send('hello');
res.setHeader('Content-Type', 'application/json'); // Can't set headers...

중복 응답 제거 — 가드를 추가

중복 콜백 호출로 두 번 응답이 나가는 것을 방지하려면 ‘한 번만 응답한다’는 가드를 두십시오.

1
2
3
4
5
6
let responded = false;
function done(payload) {
  if (responded) return;   // 이미 응답했으면 무시
  responded = true;
  res.json(payload);
}

redirect 후 코드 경로 정리

1
2
3
4
5
if (!req.user) {
  return res.redirect('/noauth');   // return으로 이후 실행 차단
}
// 여기까지 왔다면 재귀/추가 응답 없이 단일 흐름 유지
res.json({ ok: true });

return을 쓰면 redirect 후에 아래 코드라 실행되는 것을 막아 ‘redirect + 추가 응답’ 조합을 피할 수 있습니다.

5. 향후 예방 조치

  • 모든 라우터에서 응답 함수는 정확히 한 번, 그리고 마지막에 호출하도록 규약화하십시오.
  • 비동기 콜백이 두 번 실행될 수 있는 로직(중복 조건, next() 혼용)은 flag 가드로 방어하십시오.
  • res.setHeader/res.writeHead는 본문 작성 전에만 사용하고, 상태는 route 초반에 결정하십시오.
  • 미들웨어 체인에서 res 종료 후 다시 라우팅되는 흐름이 없는지 점검하십시오.

출처: StackOverflow — Error: Can’t set headers after they are sent to the client