HTTP Expect: 100-continue 정리: 본문을 보내기 전에 서버 응답을 기다리는 규칙
RFC 9110과 RFC 9112를 기준으로 Expect: 100-continue의 흐름, 클라이언트·오리진 서버·프록시가 지켜야 할 규칙, 417 Expectation Failed와 조기 거절 후 연결 처리 방법을 정리합니다.
수 GB짜리 파일을 PUT으로 올렸는데, 다 보내고 나서야 401(Unauthorized)이나 405(Method Not Allowed)가 돌아온다면 그동안 보낸 데이터는 전부 버려집니다. Expect: 100-continue는 이런 낭비를 줄이려고 만든 장치입니다. 클라이언트는 헤더를 먼저 보내고, 서버가 100 Continue로 “본문을 보내도 된다”고 답할 때까지 본문 전송을 미룰 수 있습니다.
이 글은 현재 HTTP 의미론 표준인 RFC 9110(2022년 6월)과 HTTP/1.1 메시지·연결 규칙인 RFC 9112(2022년 6월)만을 근거로 합니다. 특정 클라이언트 라이브러리나 서버 제품이 실제로 얼마나 기다리는지, 기본값이 무엇인지는 다루지 않습니다. 그런 동작은 표준이 정하지 않으며, 각 프로젝트의 공식 문서로 따로 확인해야 합니다.
요청 흐름
RFC 9110 10.1.1절에 실린 예시 요청은 다음과 같이 시작합니다.
PUT /somewhere/fun HTTP/1.1
Host: origin.example.com
Content-Type: video/h264
Content-Length: 1234567890987
Expect: 100-continue
RFC는 이 요청을 받은 오리진 서버가 클라이언트가 불필요한 데이터를 밀어 넣기 전에 401이나 405 같은 오류를 바로 돌려줄 수 있다고 설명합니다. 서버가 받아들일 생각이면 중간 응답 100 (Continue)를 보내고, 클라이언트는 그 응답을 버린 뒤 본문을 이어서 보냅니다. 본문을 다 받고 처리한 뒤 서버는 최종 응답을 보냅니다.
정리하면 갈림길은 두 가지입니다.
- 헤더만으로 결과가 정해지는 경우: 서버가 곧바로 최종 상태 코드(예: 401, 405)를 보냅니다.
- 본문이 필요한 경우: 서버가 곧바로
100 Continue를 보내고, 본문을 받은 뒤 최종 응답을 보냅니다.
RFC 9110은 이 방식이 효율을 높일 수 있는 경우로 데이터가 매우 클 때, 그리고 클라이언트가 오류를 예상할 때를 듭니다. 예로 든 것은 인증 정보를 아직 검증받지 않은 상태에서 상태를 바꾸는 메서드를 처음 보내는 경우입니다.
Expect 필드 자체
Expect필드 값은 대소문자를 구분하지 않습니다.- RFC 9110이 정의한 기대(expectation)는
100-continue하나뿐이고, 정의된 파라미터도 없습니다. - 문법은 목록 형태(
#expectation)입니다. RFC 9110 부록 B.3(RFC 7231 대비 변경 사항)에는 RFC 2616과의 호환을 위해 목록 문법을 되살렸다고 적혀 있습니다. 100-continue외의 값이 들어 있으면 서버는 417(Expectation Failed)로 응답할 수 있습니다(MAY).
클라이언트가 지켜야 할 규칙
RFC 9110 10.1.1절의 클라이언트 요구 사항입니다.
- 본문(content)이 없는 요청에는
100-continue를 넣으면 안 됩니다(MUST NOT). - 본문을 보내기 전에
100 Continue를 기다릴 생각이라면 반드시Expect: 100-continue를 보내야 합니다(MUST). 100-continue를 보냈다고 해서 정해진 시간만큼 기다릴 의무는 없습니다. 응답을 받지 못했어도 본문을 보내기 시작해도 됩니다(MAY).- HTTP/1.0 중개자는
100 Continue를 전달할 수 없으므로, 무한정 기다려서는 안 됩니다(SHOULD NOT). 100-continue를 담은 요청에 417을 받았다면, 기대 없이 같은 요청을 다시 보내야 합니다(SHOULD). RFC는 417이 응답 경로 어딘가(예: HTTP/1.0 서버)가 기대를 지원하지 않는다는 뜻일 뿐이라고 설명합니다.
여기서 중요한 점은 대기 시간을 표준이 숫자로 정하지 않는다는 것입니다. “얼마나 기다렸다가 그냥 보낼지”는 구현의 몫이고, 그래서 클라이언트마다 동작이 다를 수 있습니다.
RFC 9112 9.5절은 본문을 보내는 동안의 행동도 권합니다. 본문을 전송 중인 클라이언트는 오류 응답이 오는지 연결을 지켜봐야 하고(SHOULD), 서버가 본문을 원하지 않으며 연결을 닫는다는 응답을 보면 즉시 전송을 멈추고 자기 쪽 연결을 닫아야 합니다(SHOULD).
오리진 서버가 지켜야 할 규칙
HTTP/1.1 이상의 요청에서 메서드, 대상 URI, 헤더 섹션이 모두 도착했고, 그 안에 100-continue와 본문이 뒤따른다는 표시가 있다면 오리진 서버는 다음 중 하나를 보내야 합니다(MUST).
- 메서드, 대상 URI, 헤더만 보고 결정할 수 있다면 즉시 최종 상태 코드
- 그렇지 않다면 즉시
100 Continue
그리고 100 Continue를 보내기 전에 본문을 기다려서는 안 됩니다(MUST NOT). 필자가 보기에 이 규칙은 클라이언트는 100을 기다리고 서버는 본문을 기다리며 서로 멈춰 있는 상황을 막아 줍니다.
나머지 서버 요구 사항은 다음과 같습니다.
- HTTP/1.0 요청에 들어 있는
100-continue는 무시해야 합니다(MUST). 1xx 응답 자체도 HTTP/1.0 클라이언트에게 보내면 안 됩니다(RFC 9110 15.2절, MUST NOT). - 본문 일부나 전부를 이미 받았거나, 메시지 프레이밍상 본문이 없다면
100 Continue를 생략해도 됩니다(MAY). 100 Continue를 보냈다면 본문을 받아 처리한 뒤 최종 상태 코드를 반드시 보내야 합니다(MUST). 연결이 중간에 끊긴 경우는 예외입니다.- 본문을 다 읽기 전에 최종 상태 코드를 보낸다면, 연결을 닫을 것인지 계속 본문을 읽을 것인지 알려야 합니다(SHOULD).
조기 거절 뒤의 연결 처리
마지막 항목은 RFC 9112와 함께 봐야 합니다. RFC 9112 9.3절에 따르면 서버는 요청 본문을 끝까지 읽거나, 응답을 보낸 뒤 연결을 닫아야 합니다(MUST). 그러지 않으면 지속 연결에 남은 본문 데이터가 다음 요청으로 잘못 해석됩니다.
연결을 닫겠다는 의사는 Connection: close로 표시합니다(RFC 9112 9.6절). 다만 서버가 TCP 연결을 바로 끊으면, 클라이언트가 아직 보내던 데이터 때문에 서버 TCP 스택이 리셋을 보내고, 그 리셋이 클라이언트가 아직 읽지 못한 응답을 지워 버릴 수 있습니다. RFC 9112는 그래서 서버가 보통 단계적으로 연결을 닫는다고 설명합니다. 먼저 쓰기 방향만 닫고(half-close), 클라이언트가 닫거나 마지막 응답이 전달됐다고 어느 정도 확신할 때까지 계속 읽은 다음, 완전히 닫는 방식입니다.
프록시가 지켜야 할 규칙
같은 조건의 요청을 받은 프록시는 다음 중 하나를 해야 합니다(MUST).
- 메서드, 대상 URI, 헤더만으로 결정할 수 있다면 즉시 최종 상태 코드를 보냅니다.
- 그렇지 않다면 요청 줄과 헤더 섹션을 다음 인바운드 서버로 전달합니다.
다음 인바운드 서버가 HTTP/1.0만 지원한다고 판단할 근거(설정이나 이전 통신)가 있다면, 프록시가 직접 100 Continue를 만들어 보내도 됩니다(MAY).
1xx 응답은 원칙적으로 전달해야 합니다(MUST). 예외는 프록시 자신이 1xx를 유발한 경우입니다. 예를 들어 프록시가 요청을 전달하면서 Expect: 100-continue를 직접 추가했다면, 그에 대한 100 Continue는 클라이언트 쪽으로 전달하지 않아도 됩니다(RFC 9110 15.2절).
417의 정의도 이 구조와 관련이 있습니다. RFC 9110 15.5.18절은 417을 “Expect의 기대를 인바운드 서버 중 적어도 하나가 충족하지 못했다”는 뜻으로 정의합니다. 417을 보낸 쪽이 오리진 서버가 아닐 수도 있다는 뜻입니다.
1xx 응답을 다룰 때 알아 둘 점
- 1xx 응답은 헤더 섹션이 끝나면 끝납니다. 본문이나 트레일러를 가질 수 없습니다(RFC 9110 15.2절, RFC 9112 6.3절).
- 클라이언트는 최종 응답 전에 1xx가 하나 이상 와도, 기대하지 않았더라도 파싱할 수 있어야 합니다(MUST). 사용자 에이전트는 예상치 못한 1xx를 무시해도 됩니다(MAY).
Expect: 100-continue없이 받은100 Continue는 그냥 버리면 됩니다(RFC 9110 15.2.1절).- HTTP/1.1에는 요청 식별자가 없어서, 클라이언트는 받은 응답을 아직 최종(1xx가 아닌) 응답을 받지 못한 가장 앞의 요청에 대응시켜야 합니다(RFC 9112 9.2절, MUST). 100 응답을 최종 응답으로 잘못 처리하면, 같은 연결의 이후 응답이 엉뚱한 요청에 대응될 수 있습니다.
Upgrade와Expect: 100-continue가 함께 온 요청이라면, 서버는101 (Switching Protocols)보다 먼저100 Continue를 보내야 합니다(RFC 9110 7.8절, MUST).
점검 목록
아래는 위 규칙에서 끌어낸 필자의 제안이며, RFC 문구 그대로는 아닙니다.
- 클라이언트:
100 Continue를 기다리는 시간에 상한을 두고, 시간이 지나면 본문을 보내는 동작인지 확인하세요. 417을 받으면Expect없이 한 번 다시 보내는 경로가 있는지도 확인하세요. - 클라이언트: 본문이 없는 요청(예: 본문 없는 GET)에
Expect를 붙이지 않는지 확인하세요. - 오리진 서버: 인증, 권한, 메서드 허용 여부, 크기 제한처럼 헤더만으로 판단할 수 있는 검사는 본문을 읽기 전에 하세요. 그래야 표준이 요구하는 “즉시 최종 응답 또는 즉시 100”을 지킬 수 있습니다.
- 오리진 서버: 본문을 읽기 전에 거절한다면
Connection: close를 보내고 단계적으로 닫거나, 남은 본문을 끝까지 읽어 버리세요. 둘 중 무엇도 하지 않으면 남은 데이터가 다음 요청으로 해석될 수 있습니다. - 프록시와 게이트웨이: 1xx를 전달하는지, 직접
Expect를 추가한다면 그 100 응답을 어떻게 처리하는지 설정을 확인하세요.
범위와 한계
- 이 글은 RFC 9110과 RFC 9112의 규칙만 다룹니다. RFC 9110은 요구 사항을 “HTTP/1.1 (or later)” 요청 기준으로 서술하지만, HTTP/2와 HTTP/3에서 메시지가 어떻게 프레이밍되는지는 각각의 RFC에서 정하며 이 글에서는 확인하지 않았습니다.
- 특정 HTTP 클라이언트·서버·프록시의 기본 대기 시간이나
Expect자동 추가 여부는 제품마다 다르고, 이 글에서는 다루지 않습니다.
참고 자료
- RFC 9110, HTTP Semantics (STD 97, 2022년 6월): 7.8절 Upgrade, 10.1.1절 Expect, 15.2절 Informational 1xx, 15.2.1절 100 Continue, 15.5.18절 417 Expectation Failed, 부록 B.3. https://www.rfc-editor.org/rfc/rfc9110.txt
- RFC 9112, HTTP/1.1 (STD 99, 2022년 6월): 6.3절 Message Body Length, 9.2절 Associating a Response to a Request, 9.3절 Persistence, 9.5절 Failures and Timeouts, 9.6절 Tear-down. https://www.rfc-editor.org/rfc/rfc9112.txt