본문 바로가기
빌드노트 검색

빌드노트

Node 응답 headers에 Trailer 체크섬이 없는 이유

Node HTTP 응답의 Trailer 헤더는 뒤에 보낼 필드 이름을 예고한다. 실제 체크섬은 headers가 아니라 본문 소비 후 end의 trailers에서 읽는다. 같은 chunked 본문에 실제 전송·예고만 함·트레일러 없음 세 조건을 비교해 조회 시점과 미전송을 구별한다.

Node의 http.get() 응답에 Trailer: X-Demo-Checksum이 있는데 res.headers['x-demo-checksum']는 undefined일 수 있다. 트레일러로 보낸 값은 본문 뒤에 도착하며, 본문을 끝까지 소비한 뒤 end 콜백에서 res.trailers를 확인해야 한다. 끝까지 기다려도 그 값이 res.headers에 합쳐지는 것은 아니다.

Trailer 헤더의 값은 뒤에 보낼 필드의 이름이다. X-Demo-Checksum이라는 이름을 예고한 것이지, 체크섬 값을 미리 담은 것이 아니다. RFC 9110의 Trailer 정의 (새 탭에서 열림)는 이 예고만으로 해당 필드의 실제 전송이 보장되지는 않는다고 설명한다.

응답 콜백에서 읽던 값을 end 안에서 다시 확인한다

기존 node:http 요청의 응답 콜백, 즉 http.get(..., (res) => { ... }) 안에 다음 조회를 넣는다. 여기서 res는 클라이언트가 받은 http.IncomingMessage다. 아래 코드는 작은 진단용 응답의 본문을 버리고 필드만 확인한다. 본문을 저장하거나 파싱하는 코드가 이미 있다면 마지막 res.resume() 대신 기존 본문 소비 코드를 유지한다.

console.log('Trailer:', res.headers.trailer);
console.log('headers:', res.headers['x-demo-checksum']);
console.log('before:', JSON.stringify(res.trailers));
res.once('end', () => {
  console.log('headers at end:', res.headers['x-demo-checksum']);
  console.log('after:', JSON.stringify(res.trailers));
  console.log('raw:', JSON.stringify(res.rawTrailers));
});
res.resume();

Node v22.23.2의 trailers 문서 (새 탭에서 열림)는 end에서 값이 채워진다고 설명한다. end 리스너만 달고 본문을 읽지 않으면 기다리던 이벤트가 오지 않는다. ClientRequest 설명 (새 탭에서 열림)처럼 data 처리, read() 또는 resume()으로 응답을 소비해야 한다. resume()은 본문을 보관해 주지 않는다.

같은 본문에서 트레일러 전송만 바꿨다

Windows의 Node.js v22.23.2에서 새 서버를 127.0.0.1의 임의 포트에 열어 HTTP/1.1로 비교했다. 세 응답 모두 Transfer-Encoding: chunked였고 본문은 hello 뒤에 줄바꿈 하나가 붙은 hello\n이었다. 클라이언트는 TE: trailers, Connection: TE, close를 보냈다. 다음은 트레일러를 실제로 보낸 응답에서 위 조회 코드가 출력한 내용이다.

Trailer: X-Demo-Checksum
headers: undefined
before: {}
headers at end: undefined
after: {"x-demo-checksum":"sum8=1e"}
raw: ["X-Demo-Checksum","sum8=1e"]

일반 헤더의 체크섬 조회는 전후 모두 undefined였다. 달라진 곳은 res.trailers다. 이 객체에서는 소문자 키 x-demo-checksum으로 값을 읽었다. headers 문서 (새 탭에서 열림)도 헤더 이름을 소문자 키로 제공한다고 설명한다. 원래 필드 표기를 확인하려면 rawTrailers (새 탭에서 열림)를 본다. 이번에는 이름과 값이 번갈아 들어 있는 배열에 X-Demo-Checksum이 남았다.

같은 서버에서 두 대조 조건도 실행했다.

  • Trailer: X-Demo-Checksum을 유지하고 실제 트레일러만 보내지 않으면, Trailer: 출력은 같았지만 after: {}, raw: []였다.
  • 예고 헤더와 실제 트레일러를 모두 빼면, Trailer: undefined였고 after: {}, raw: []였다.

두 대조 응답도 같은 본문을 끝까지 받았다. 따라서 before: {}만 보고 필드가 누락됐다고 판단할 수는 없다. 반면 정상적으로 본문을 소비한 end에서 after: {}라면, 조회를 늦추는 것만으로 해결되지 않는다. 그 응답에 해당 후행 필드가 도착했는지 확인해야 한다.

서버의 예고 헤더와 실제 전송 코드는 별개다

서버를 직접 고칠 수 있다면 응답을 만드는 곳에서 Trailer 설정과 addTrailers() 호출을 함께 확인한다. 다음은 기존 http.createServer((req, res) => { ... })의 응답 처리 안에서 같은 합성 본문과 트레일러를 보내는 코드다. 여기의 res는 서버의 http.ServerResponse다.

res.writeHead(200, {
  'Content-Type': 'text/plain; charset=utf-8',
  'Transfer-Encoding': 'chunked',
  'Trailer': 'X-Demo-Checksum'
});
res.write('hello');
res.write('\n');
res.addTrailers({ 'X-Demo-Checksum': 'sum8=1e' });
res.end();

이 코드에서 예고는 writeHead()가, 실제 후행 필드의 추가는 addTrailers()가 맡는다. 실제 값을 추가한 다음 end()로 응답을 마친다. Node의 addTrailers 문서 (새 탭에서 열림)는 chunked 응답에서만 트레일러를 내보낸다고 설명한다. 위 HTTP/1.1 예제에는 Content-Length를 함께 설정하지 않는다. 아무 헤더나 뒤로 옮겨도 된다는 뜻도 아니다. RFC 9110의 사용 제한 (새 탭에서 열림)은 그 필드의 정의가 트레일러 사용을 허용해야 한다고 규정한다.

여기서 X-Demo-Checksum은 실습용 필드이고, sum8=1e는 합성 본문 바이트 합의 하위 8비트를 16진수로 표시한 값이다. 값을 어디서 읽는지 확인하기 위한 예제이며 인증이나 보안상 무결성을 보장하지 않는다.

자기 응답에서도 end 뒤에 필드가 없다면 서버의 실제 전송 경로와 응답 형식을 먼저 확인한다. 프록시를 거친다면 서버가 보낸 값이 클라이언트까지 유지되는지도 별도로 검사한다. RFC 9110 (새 탭에서 열림)은 중간 전달 과정에서 트레일러가 버려질 수 있음을 설명한다. 이번에 실행한 범위는 직접 연결한 Node HTTP/1.1뿐이다. 프록시, 브라우저·Fetch, HTTP/2·HTTP/3, 전송 중단 시 동작은 검증하지 않았다.

← 목록으로 돌아가기