HTTP 103 Early Hints: 최종 응답보다 먼저 보내는 Link 헤더와 클라이언트가 지켜야 할 선
RFC 8297과 RFC 9110을 기준으로 103 Early Hints가 무엇을 보내는지, 클라이언트가 힌트를 어디까지 써도 되는지, 103을 여러 번 보낼 때와 HTTP/1.1에서 조심할 점을 정리했습니다.
HTML 응답을 만드는 데 데이터베이스 조회가 300ms 걸린다고 해 봅시다. 그 HTML이 불러올 CSS와 JavaScript 파일은 요청을 받는 순간 이미 정해져 있는데도, 서버는 그동안 Link 헤더 하나 보내지 못합니다. 최종 응답의 헤더는 상태 코드와 함께 한 번에 나가야 하고, 상태 코드는 조회 결과에 따라 200이 될지 404가 될지 아직 모르기 때문입니다.
RFC 8297이 정의한 103 Early Hints는 이 공백을 메우는 상태 코드입니다. 최종 응답에 들어갈 가능성이 높은 헤더 필드를 정보성(1xx) 응답에 담아 먼저 보내고, 서버는 그 뒤에 시간이 걸리는 작업을 계속합니다.
주고받는 모양
RFC 8297 2절의 예를 그대로 옮기면 이렇습니다.
GET / HTTP/1.1
Host: example.com
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
HTTP/1.1 200 OK
Date: Fri, 26 May 2017 10:02:11 GMT
Content-Length: 1234
Content-Type: text/html; charset=utf-8
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
<!doctype html>
[... 본문 생략 ...]
103 응답은 헤더만 있고 본문이 없습니다. 클라이언트는 103의 Link 필드에서 rel=preload를 알아보고 /style.css를 미리 가져오기 시작할 수 있습니다. 클라이언트가 힌트를 쓴다면 시간 순서는 대략 다음과 같습니다.
클라이언트 서버
|---- GET / ----------------------->|
|<--- 103 Early Hints (Link) -------| 요청을 받자마자 보냄
| | DB 조회 중
|---- GET /style.css -------------->| 힌트를 보고 미리 요청
|<--- 200 OK (HTML) ----------------| 조회가 끝난 뒤 최종 응답
Link 필드의 문법 자체는 RFC 8288 3절의 것입니다. <URI> 뒤에 ;로 구분한 파라미터가 붙고, 상대 URI는 RFC 3986 규칙대로 해석합니다. preload 관계와 as 속성은 RFC 8297이 참조하는 W3C Preload 명세의 것이라 이 글에서는 다루지 않습니다.
힌트는 어디까지나 힌트
RFC 8297 2절에서 가장 중요한 문장은 103의 헤더가 최종 응답의 헤더를 대신하지 않는다는 것입니다. 정리하면 다음과 같습니다.
- 최종 응답 처리에 영향을 주면 안 됩니다(MUST NOT). 클라이언트는 103의 헤더를 보고 미리 무언가를 가져오는 식의 성능 최적화는 할 수 있지만, 그 밖에는 최종 응답을 처리하는 방식이 103 때문에 달라져서는 안 됩니다.
- 103 자신에 대한 메타데이터로 읽으면 안 됩니다(MUST NOT). 103에 담긴
Content-Type이나 다른 필드는 103 응답을 설명하는 것이 아니라 앞으로 올 최종 응답에 대한 예고입니다. - 빠진 필드에서 아무것도 추측하지 않아야 합니다(SHOULD NOT). 서버는 최종 응답에 들어갈 필드 중 일부만 103에 넣어도 됩니다(MAY). 103에 어떤 필드가 없다고 해서 최종 응답에도 없을 것이라고 해석하면 안 됩니다.
- 최종 응답과 달라질 수 있습니다. 보통은 103에 보낸 필드를 최종 응답에도 다시 넣지만, 서버가 최종 응답을 보내기 전에 103의 내용이 틀렸다는 것을 알게 되면 다르게 보낼 수 있다고 명세는 적고 있습니다.
서버 쪽에서 보면, 클라이언트에게 실제로 적용되어야 하는 헤더는 103에 넣었더라도 최종 응답에 다시 넣어야 한다는 뜻입니다. 103은 최종 응답의 헤더를 미리 알려 줄 뿐, 그 헤더를 미리 적용해 주지는 않습니다.
103을 여러 번 보낼 때
다른 1xx 응답과 마찬가지로 103도 최종 응답 전에 여러 번 올 수 있습니다. 명세는 두 경우를 듭니다.
- 처리하는 도중 새 정보가 생길 때마다 서버가 103을 추가로 보내는 경우(MAY). 이미 보낸 필드를 다시 보낼 필요는 없지만, 다시 보내도 됩니다.
- 캐시하는 중개자가 오래된(stale) 캐시 응답의 헤더로 103을 만들어 보내고, 재검증 요청에 대해 원 서버가 보낸 103과 최종 응답을 이어서 전달하는 경우.
클라이언트는 여러 103에서 받은 필드를 어떤 조합으로든 고려해 최종 응답에 올 필드를 예상할 수 있습니다. 명세의 두 번째 예는 예상이 빗나가는 경우를 보여 줍니다.
HTTP/1.1 103 Early Hints
Link: </main.css>; rel=preload; as=style
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
HTTP/1.1 200 OK
Date: Fri, 26 May 2017 10:02:11 GMT
Content-Length: 1234
Content-Type: text/html; charset=utf-8
Link: </main.css>; rel=preload; as=style
Link: </newstyle.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
103 두 개로 예고한 Link 세 개 중 /main.css와 /script.js는 최종 응답에도 있지만, /style.css는 /newstyle.css로 바뀌었습니다. 클라이언트가 /style.css를 미리 받아 두었다면 그 요청은 결과적으로 쓸모가 없었던 셈입니다. 명세는 이런 경우를 금지하지 않으므로, 103에 넣을 필드는 최종 응답에도 들어갈 가능성이 충분히 높은 것으로 고르는 편이 낭비가 적습니다.
1xx 응답에 공통으로 적용되는 규칙
RFC 8297은 1xx의 일반 규칙을 당시의 RFC 7231에서 가져왔습니다. RFC 7231은 이제 RFC 9110으로 대체되었고, 해당 내용은 RFC 9110 15.2절에 있습니다.
- 1xx 응답은 헤더 섹션이 끝나면 끝납니다. 본문(content)도 트레일러도 담을 수 없습니다.
- 클라이언트는 최종 응답 전에 오는 1xx 응답을 하나 이상, 예상하지 못했더라도 파싱할 수 있어야 합니다(MUST). 사용자 에이전트는 예상하지 못한 1xx 응답을 무시해도 됩니다(MAY).
- 프락시는 스스로 1xx 생성을 요청한 경우가 아니면 1xx 응답을 전달해야 합니다(MUST).
- HTTP/1.0에는 1xx 상태 코드가 정의되어 있지 않았으므로, 서버는 HTTP/1.0 클라이언트에게 1xx 응답을 보내면 안 됩니다(MUST NOT).
같은 15장 첫머리는 하나의 요청에 0개 이상의 1xx 중간 응답과 정확히 하나의 최종 응답이 대응한다고 정리합니다. 103은 성능 최적화 말고는 최종 응답 처리에 영향을 주면 안 되므로, 103을 이해하지 못해 무시하는 사용자 에이전트도 최종 응답만 제대로 처리하면 됩니다. 문제는 1xx를 파싱조차 제대로 하지 못하는 클라이언트입니다.
HTTP/1.1에서의 위험
RFC 8297 3절(보안 고려 사항)은 이 지점을 직접 다룹니다. 요청에 Expect 헤더가 없을 때 정보성 응답이 오는 일은 드물었기 때문에, 103을 제대로 다루지 못하는 클라이언트가 있을 수 있다는 것입니다.
특히 HTTP/1.1 클라이언트가 103을 최종 응답으로 잘못 처리하면, 같은 연결에서 이어지는 요청의 응답이 모두 그 “최종 응답”의 일부라고 여길 가능성이 큽니다. 응답 경계가 한 칸씩 밀리는 것입니다. 클라이언트가 출처(origin)가 다른 요청을 하나의 지속 연결에 섞어 보낸다면, 이것은 출처 간 정보 노출 취약점이 될 수 있습니다.
그래서 명세는 클라이언트가 정보성 응답을 올바르게 처리한다는 것을 알지 못하는 한, 서버가 HTTP/1.1로는 103을 보내지 않을 수도 있다고 적습니다. HTTP/2 클라이언트는 응답 헤더를 처리하는 방식이 본문의 끝을 정하는 방식에 영향을 주지 않으므로 이런 프레이밍 오류를 겪을 가능성이 더 낮다고 덧붙입니다.
서버 푸시와 비교
RFC 8297 1절은 HTTP/2 서버 푸시와도 비교합니다. 2017년 당시 명세가 든 서버 푸시의 한계는 두 가지입니다. 서버가 권한을 가진(authoritative) 리소스만 보낼 수 있고, 클라이언트가 이미 캐시하고 있어도 응답이 전송된다는 것입니다. 103으로 Link 헤더를 일찍 보내는 방식은 최악의 경우 서버 푸시보다 왕복이 한 번 더 들지만, 더 유연하고 대역폭을 덜 쓸 수 있다고 명세는 설명합니다. 앞의 두 한계와 맞대어 보면, 요청을 보낼지 말지를 클라이언트가 정하므로 이미 캐시에 있는 리소스는 다시 받지 않을 수 있다는 뜻으로 읽힙니다.
같은 절은 원 서버가 103을 이용해 캐시하는 중개자에서 HTTP/2 서버 푸시를 일으킬 수도 있다고 적고 있습니다.
명세의 위치와 이 글의 범위
RFC 8297은 표준 트랙이 아니라 실험적(Experimental) 문서입니다. 103이라는 번호와 “Early Hints”라는 이름은 이 문서로 IANA HTTP 상태 코드 레지스트리에 등록되었습니다(4절).
이 글은 RFC 8297, RFC 9110 15장, RFC 8288 3절만을 근거로 합니다. 어떤 브라우저나 CDN, 웹 서버가 103을 지원하는지, 브라우저가 103에서 preload 외의 어떤 필드나 관계를 실제로 처리하는지는 명세에 없는 내용이라 다루지 않았습니다. 이 부분은 쓰려는 구현의 공식 문서에서 확인해야 합니다.
참고한 문서
- RFC 8297 An HTTP Status Code for Indicating Hints: 1, 2, 3, 4절
- RFC 9110 HTTP Semantics: 15장 첫머리, 15.2절
- RFC 8288 Web Linking: 3, 3.1절
링크와 내용은 2026년 10월 20일에 확인했습니다.