빌드노트
HEAD 응답에 Content-Length는 있는데 본문이 없다
HEAD 응답의 Content-Length는 같은 요청을 GET으로 보냈을 때의 본문 길이를 나타낼 수 있다. 실제 GET·HEAD의 수신 바이트와 curl -I·-i를 비교하고, 빈 HEAD 본문을 전송 잘림이나 대기 오류로 오해하지 않는 판단을 설명한다.
curl -I로 확인한 응답은 200이고 Content-Length: 13도 있는데, 정작 본문은 한 바이트도 오지 않는다. 이때 길이만 보고 13바이트를 기다리거나 전송이 잘렸다고 판단하기 전에 요청 메서드가 HEAD인지 확인한다. HEAD 응답에는 본문이 없다. 본문을 받아야 한다면 GET으로 요청해야 한다.
RFC 9110 §9.3.2 (새 탭에서 열림)는 HEAD를 GET에 대응하는 메타데이터를 얻는 메서드로 설명하며, 서버가 응답 본문을 보내는 것을 금지한다. HEAD 응답에 Content-Length를 넣을 수는 있다. 이때 값은 같은 요청을 GET으로 보냈다면 받았을 본문의 바이트 수여야 한다. 지금 HEAD 응답 뒤에 그만큼의 바이트가 따라온다는 뜻이 아니다. Content-Length 규칙 (새 탭에서 열림)
같은 200과 길이 헤더, 다른 수신 바이트
이를 확인하려고 127.0.0.1의 임의 포트에 작은 HTTP/1.1 서버를 열었다. GET은 고정 UTF-8 본문 안녕, HEAD\n을 보내고, HEAD는 같은 상태·Content-Type·Content-Length를 보내되 본문을 쓰지 않게 했다. 여기서 \n은 마지막 줄바꿈 한 바이트를 표시한다. 운영 장애 기록이 아니라 따로 만든 비교 실험이다.
Windows 11·Python 3.12.10에서 원시 TCP 클라이언트로 두 메서드를 보냈다. 각 요청에는 Connection: close를 넣고 연결이 끝날 때까지 받아, 헤더 끝의 빈 줄 뒤에 실제로 온 바이트를 셌다. 두 응답 모두 Content-Type: text/plain; charset=utf-8이었고, 결과는 다음과 같았다.
요청 상태 Content-Length 실제 본문 바이트
GET 200 13 13
HEAD 200 13 0
GET에서는 마지막 줄바꿈까지 받았고 HEAD에서는 헤더 뒤에 아무 바이트도 없었다. 200과 Content-Length만으로는 이번 응답에 본문이 있는지 결정할 수 없었다. 같은 상태와 길이에서도 요청 메서드에 따라 읽기를 끝낼 위치가 달랐다.
curl의 -I와 -i를 구별한다
자기 서버에서도 확인하려면 curl 실행 파일과, GET으로 요청했을 때 200 및 본문을 돌려주는 테스트 주소가 필요하다. 아래는 PowerShell 7에서 실행한 입력이다. $url은 실험 당시의 일회성 loopback 주소이므로 자기 테스트 주소로 바꾼다. 이 실험 서버는 측정 후 종료했다.
$url = 'http://127.0.0.1:39667/hello'
curl.exe -q --noproxy '*' --http1.1 --connect-timeout 1 --max-time 2 -sS -I -w '\nbody_bytes=%{size_download}\n' $url
대문자 -I는 HTTP HEAD를 보낸다. curl 8.21.0에서 위 입력의 출력은 다음과 같았고 종료 코드는 0이었다.
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 13
body_bytes=0
이번에는 같은 주소에 소문자 -i를 사용했다. 다른 메서드 지정이나 데이터 전송 옵션이 없는 이 명령은 GET을 보내고, 응답 헤더도 본문과 함께 표시한다.
curl.exe -q --noproxy '*' --http1.1 --connect-timeout 1 --max-time 2 -sS -i -w '\nbody_bytes=%{size_download}\n' $url
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 13
안녕, HEAD
body_bytes=13
GET도 종료 코드는 0이었다. body_bytes는 헤더 값의 복사가 아니라 curl의 size_download 측정값이다. 응답 헤더를 제외한 전송 본문 바이트를 센다. -I 출력에는 헤더 문자가 있으므로 화면에 글자가 보이거나 리다이렉트한 파일이 비어 있지 않다고 해서 본문까지 받았다고 볼 수 없다. curl의 --head·--show-headers·--write-out 설명 (새 탭에서 열림)
명령의 -q는 기본 curl 설정 파일을 읽지 않게 하고, --noproxy '*'는 이 직접 연결에서 프록시 사용을 막는다. 연결 제한은 1초, 한 번의 전송 전체 제한은 2초다. 자기 서버의 본문은 예제의 13바이트와 달라도 된다. 먼저 실제 요청이 HEAD인지 GET인지, 응답 상태가 200인지, curl이 오류 없이 끝났는지를 확인한다. HEAD를 지원하지 않아 405나 501을 받았거나 시간 초과됐다면 이 실험의 정상 HEAD 결과와 같은 조건이 아니다.
메서드 이름을 직접 쓰려고 -X HEAD로 바꾸는 것도 주의한다. curl 공식 문서는 -X가 요청의 메서드 문자열만 바꾸며 curl의 응답 처리 방식까지 바꾸지는 않는다고 설명한다. HEAD를 조회할 때는 -I 또는 --head를 사용한다. 이 차이는 문서에서 확인한 동작이며, 위 두 명령에서 실행한 옵션은 각각 -I와 -i다. curl --request (새 탭에서 열림)
길이를 읽기 전에 HEAD 응답인지 판단한다
연결 종료가 없으면 어떻게 되는지도 별도로 비교했다. 같은 서버에 연결을 유지하는 요청을 보내고, 원시 클라이언트의 읽기 방식을 바꿨다. 헤더 숫자만 보고 그만큼 읽는 구현은 GET에서 13바이트를 받고 끝났다. HEAD에서는 0바이트인 채 다음 데이터를 기다리다가, 설정한 0.35초 소켓 읽기 제한에 걸렸다.
반면 요청 메서드를 함께 보관한 구현은 HEAD의 응답 헤더가 완성된 지점에서 처리를 끝냈다. 본문용 읽기를 추가로 호출하지 않았고 시간 초과도 없었다. 여기서 시간 초과는 본문이 전송 중 유실됐다는 증거가 아니었다. 서버가 보내면 안 되는 본문을 클라이언트가 기다리고 있었다.
RFC 9112 §6.3 (새 탭에서 열림)은 HTTP/1.1에서 HEAD에 대한 응답이 헤더 뒤 첫 빈 줄로 끝난다고 정한다. 어떤 헤더가 있든 이 규칙이 먼저 적용된다. 따라서 직접 응답을 읽는 코드에서는 Content-Length만 전달하지 말고, 그 응답에 대응하는 요청 메서드도 함께 확인해야 한다. HEAD라면 헤더를 모두 읽은 뒤 본문 수신을 기다리지 않는다. 연결이 닫히는 것까지 기다릴 필요도 없다.
서버 쪽에서 빈 본문에 맞추겠다며 Content-Length를 무조건 0으로 바꿀 이유도 없다. 같은 요청의 GET 본문이 13바이트라면 HEAD에 보낼 길이도 13이어야 한다. HEAD에서 길이 헤더를 생략하는 것은 허용되므로, 헤더가 없다는 사실만으로 오류라고 판단해서도 안 된다. 본문이 필요한 작업은 GET으로 바꾸고, 헤더 정보만 필요한 작업은 HEAD의 빈 본문을 정상으로 처리한다.
실제 요청이 GET인데 이 실험처럼 압축·Transfer-Encoding 없이 Content-Length가 양수이고 수신 본문이 부족하다면, HEAD의 규칙으로 정상 처리할 수 없다. 그때는 전송 오류와 서버가 선언한 길이를 조사한다. 반대로 HEAD의 0바이트만을 근거로 다운로드 실패를 기록한다면 먼저 그 판정 코드를 고친다.
이번에 확인한 범위는 고정 본문 하나를 제공하는 직접 연결의 HTTP/1.1 200 응답이다. 압축, TLS, HTTP/2·3, 프록시·캐시, 실제 서비스의 HEAD 구현은 실행하지 않았다. 원시 클라이언트도 이 응답 조건만 비교하는 도구이며 범용 HTTP 파서로 검증한 것은 아니다.