HTTP Range 요청과 206·416: 끊긴 다운로드는 어떻게 이어받을까

끊긴 다운로드를 이어받을 때 오가는 Range, Content-Range, If-Range 헤더와 206 Partial Content, 416 Range Not Satisfiable의 규칙을 RFC 9110으로 정리했습니다. 서버가 Range를 무시하는 경우도 다룹니다.

개발웹네트워크

10,000바이트짜리 파일에서 처음 6,000바이트를 받은 뒤 연결이 끊겼다고 해 봅시다. 처음부터 다시 받지 않고 남은 4,000바이트만 달라고 할 수 있게 해 주는 것이 HTTP Range 요청입니다. 규칙은 HTTP 의미론 명세인 RFC 9110의 14절에 있습니다. Range 요청은 선택 기능이라 서버는 Range 헤더를 무시하고 일반 GET처럼 전체를 보내도 됩니다. 명세가 Range 처리를 정의한 메서드는 GET 하나뿐입니다.

바이트 범위 쓰는 법

Range: bytes=0-499는 처음 500바이트입니다. 바이트 위치는 0부터 세고 양 끝을 포함합니다. bytes=6000-처럼 끝을 비우면 6000번 바이트부터 끝까지이고, 끝 값이 실제 길이 이상이어도 끝까지로 해석됩니다. bytes=-500처럼 앞을 비우면 마지막 500바이트이며, 파일이 그보다 짧으면 전체가 대상입니다. gzip 같은 콘텐츠 코딩이 적용된 응답이라면 범위는 풀기 전, 인코딩된 바이트를 기준으로 셉니다.

206 응답과 Content-Range

서버가 범위를 받아들이면 206 Partial Content로 답합니다.

HTTP/1.1 206 Partial Content
Content-Range: bytes 6000-9999/10000
Content-Length: 4000

Content-Range는 시작-끝/전체 길이 형식이고, 전체 길이를 모르면 /*를 씁니다. Content-Length는 이번 본문의 길이이지 파일 전체 길이가 아닙니다. 서버는 요청받은 범위 중 일부만 보낼 수도 있어서, 클라이언트는 Content-Range를 보고 무엇을 받았는지 확인해야 합니다.

bytes=0-99,5000-5099처럼 여러 범위를 요청했고 서버가 둘 이상의 파트를 보내면, 본문은 multipart/byteranges 형식이 되고 Content-Range는 최상위 헤더가 아니라 각 파트에 들어갑니다. 서버는 겹치거나 가까운 범위를 합칠 수 있습니다. 여러 파트의 순서는 요청 순서를 따르는 것이 권장되지만, 합쳐지거나 충족할 수 없는 범위는 제외될 수 있으므로 요청한 목록과 일대일로 대응한다고 가정하면 안 됩니다. 범위를 하나만 요청했다면 서버는 multipart로 답하면 안 됩니다.

416과 200

요청한 범위가 하나도 충족되지 않으면 416 Range Not Satisfiable입니다. 바이트 범위가 충족되려면 시작 위치가 현재 길이보다 작거나, 길이가 0이 아닌 끝부분 범위여야 합니다. 길이 10,000인 파일에 bytes=10000-을 요청하면 충족되지 않으므로, 서버는 Content-Range: bytes */10000으로 현재 길이를 알려 주는 것이 권장됩니다. 작은 범위를 지나치게 많이 요청하거나 겹치는 범위를 요청해도 416으로 거절할 수 있습니다. 명세는 이런 요청을 고장 난 클라이언트나 서비스 거부 공격의 징후로 봅니다.

하지만 명세 스스로 많은 구현이 이럴 때 전체를 200으로 보낸다며, 클라이언트가 416을 기대할 수 없다고 적습니다. 서버 설정도 제각각입니다. nginx의 max_ranges는 기본적으로 제한이 없고, 한도를 넘으면 범위가 없는 요청처럼 처리하며, 0이면 범위 지원을 끕니다. Apache HTTP Server의 MaxRanges는 기본값이 200이고, 넘으면 전체 리소스를 보냅니다. Range를 보내는 쪽은 206, 200, 416을 모두 처리할 수 있어야 합니다.

If-Range: 그사이 파일이 바뀌었다면

6,000바이트를 받은 뒤 서버의 파일이 새 버전으로 바뀌었다면, 이어 붙인 결과는 어느 버전도 아닙니다. 명세는 조각들이 같은 강한 검증자(strong validator)를 가질 때만 합칠 수 있다고 합니다. 이 확인과 재요청을 한 번에 처리하는 헤더가 If-Range입니다.

GET /file.zip HTTP/1.1
Range: bytes=6000-
If-Range: "v1"

10,000바이트 파일 중 0–5999는 이미 받았고 6000–9999를 Range로 요청하는 그림. If-Range의 ETag가 같고 서버가 Range를 처리하면 206과 Content-Range: bytes 6000-9999/10000, 다르면 Range를 무시하고 보통 200으로 새 파일 전체를 받는다.

ETag가 여전히 "v1"이고 서버가 Range를 처리하면 나머지가 206으로 옵니다. 값이 다르면 서버는 Range를 반드시 무시합니다. Range가 없었다면 200으로 답할 GET이라면 새 버전 전체가 200으로 옵니다. ETag는 강한 비교로 맞춰 보며, W/로 시작하는 약한 ETag는 If-Range에 쓸 수 없습니다. Last-Modified 날짜는 ETag가 없고 그 날짜가 강한 검증자일 때만 쓸 수 있고, 정확히 같아야 참입니다.

Accept-Ranges: bytes는 안내일 뿐입니다. 이 헤더를 받았다고 다음 Range 요청이 206으로 돌아온다고 가정하면 안 됩니다. 캐시는 206 응답을 미완성 200 응답처럼 저장할 수 있지만, 부분 응답을 클라이언트에 보낼 때는 반드시 206으로 표시해야 합니다(RFC 9111). ETag와 304는 no-cache와 no-store의 차이에서 다뤘습니다.

참고한 문서

링크와 내용은 2026년 10월 5일에 확인했습니다.

블로그로