빌드노트
Retry-After가 날짜로 오면 429를 바로 재요청하는 이유
Retry-After를 정수로만 읽고 실패를 0으로 바꾸면 정상적인 날짜 응답도 즉시 재요청으로 이어진다. 429·503의 실제 loopback 헤더와 고정 시계 계산을 대조하고, 유효한 0·누락·잘못된 값 및 대기 예산 초과를 구분한다.
429를 받은 재시도 코드가 Retry-After를 정수로만 읽으면, 정상적인 날짜 응답에서도 기다리지 않을 수 있다. 숫자 변환에 실패했을 때 지연을 0으로 바꾸는 코드가 문제다. 이 헤더는 초 단위 정수와 HTTP 날짜를 모두 허용한다. 두 형식을 나누어 읽고, 해석할 수 없는 값은 유효한 0과 구별해야 한다.
기존 코드에서는 헤더를 읽는 부분과 예외 처리부터 확인한다. 다음은 Python 3.12에서 날짜를 0초로 바꾸는 문제를 확인하는 짧은 입력이다. 빈 작업 폴더에 아래 내용을 retry_check.py로 저장하고 python retry_check.py로 실행하면 마지막에 0이 출력된다. 추가 패키지는 필요 없다.
raw = "Wed, 21 Oct 2026 12:01:30 GMT"
try:
delay = int(raw)
except ValueError:
delay = 0
print(delay)
서버가 즉시 다시 요청하라고 한 결과가 아니다. 클라이언트가 날짜 변환을 시도하지 않고 실패를 0초로 바꾼 결과다. 날짜가 와도 예외만 삼키고 재시도 루프로 돌아간다면 이 경로를 먼저 고친다.
숫자 90과 날짜가 같은 대기 시간을 나타냈다
RFC 9110의 Retry-After 정의 (새 탭에서 열림)에 따르면 숫자는 응답을 받은 뒤 기다릴 초 수이고, 날짜는 다시 요청할 시각이다. delay-seconds는 음수가 아닌 십진 정수이므로 0은 유효하지만 -1과 1.5는 그 형식에 맞지 않는다. 503에서는 예상되는 서비스 이용 불가 기간을 알려 준다. 429도 이 헤더를 포함할 수 있지만 필수는 아니다. RFC 6585의 429 정의 (새 탭에서 열림)
Windows와 Python 3.12.10에서 IPv4 loopback 서버로 별도 실험했다. 서버는 429와 503 각각에 같은 여덟 조건을 응답했다. 응답의 Date도 고정했고, 파서에는 현재 시각을 2026-10-21 12:00:00 UTC로 직접 전달했다. 다음은 실제로 받은 응답에서 기록한 상태 코드와 헤더 값이다.
status=429
Retry-After: Wed, 21 Oct 2026 12:01:30 GMT
아래는 수신한 헤더 값에 대해 계산한 지연이다. 429와 503에서 결과가 같았다. None은 헤더에서 사용할 지연을 얻지 못했다는 뜻으로 정했다.
수신 값 계산한 초
90 90
Wed, 21 Oct 2026 12:01:30 GMT 90
Wed, 21 Oct 2026 11:59:30 GMT 0
0 0
not-a-date None
헤더 없음 None
-1 None
1.5 None
이는 90초를 실제로 기다린 기록이 아니다. 16개 응답의 헤더를 수신한 뒤 고정 시계로 계산했고, 재요청과 대기는 실행하지 않았다. 정수 전용 코드도 같은 수신 값에 적용했는데, 미래 날짜에서 0을 반환했다. 두 코드는 숫자 90에서는 일치하고 날짜에서 갈렸다.
정수 변환 실패 뒤에는 날짜 해석을 시도한다
수정할 분기는 다음과 같다. 앞뒤의 헤더 공백을 제거한 값이 ASCII 숫자로만 이루어졌으면 초 수로 읽는다. Python에서는 re.fullmatch(r"[0-9]+", value)처럼 전체 값을 검사할 수 있다. 그 외에는 HTTP 날짜로 해석한다. 날짜를 읽었으면 응답 수신 시각과의 차이를 구하고, 이미 지난 날짜의 차이는 0초로 처리한다. 둘 다 해석하지 못했거나 헤더가 없으면 별도의 실패 값인 None을 돌려준다.
날짜 차이 계산은 시계를 고정하면 바로 확인할 수 있다. 같은 파일 내용을 다음 블록으로 바꾸고 다시 실행한다. 두 GMT 날짜가 각각 90, 0을 출력하는지 확인한다.
import math
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
now = datetime(2026, 10, 21, 12, 0, 0, tzinfo=timezone.utc)
for raw in ("Wed, 21 Oct 2026 12:01:30 GMT",
"Wed, 21 Oct 2026 11:59:30 GMT"):
target = parsedate_to_datetime(raw)
print(max(0, math.ceil((target - now).total_seconds())))
GMT를 로컬 시각으로 바꾸어 해석하지 않는다. 두 시각 모두 시간대가 있는 값으로 비교한다. 출력이 다르면 먼저 고정한 now와 날짜 입력을 대조한다. 시간대가 없는 값과 있다는 값을 섞어 비교하다 오류가 나면 두 값의 시간대부터 맞춘다. 정수 초를 쓰는 대기 코드라서 남은 시간이 소수이면 올림했다. 추가 입력으로 현재 시각을 12:00:00.5, 목표를 12:00:01 GMT로 넣었을 때도 계산값은 1초였다.
위 블록은 날짜 차이만 확인한다. 실제 파서에는 날짜 변환 실패를 처리하는 분기가 필요하다. Python 3.12의 날짜 파싱 문서 (새 탭에서 열림)는 성공 시 datetime을 반환하고 잘못된 날짜 값에서는 ValueError가 발생할 수 있다고 설명한다. 날짜 문자열에는 쉼표가 들어가므로 쉼표 앞부분만 잘라 숫자를 찾는 방식도 피한다.
범용 클라이언트라면 현재 권장 날짜 형식만 가정해서도 안 된다. HTTP 날짜 규칙 (새 탭에서 열림)은 수신자가 오래된 RFC 850·asctime 형식도 받아들이도록 요구한다. asctime에는 시간대가 적혀 있지 않지만 HTTP에서는 UTC다. 실험용 파서에서도 두 형식의 미래 날짜를 넣어 각각 90초를 확인했다. RFC 850의 두 자리 연도는 수신 시각 기준으로 풀어야 하므로, 일반 날짜 라이브러리의 고정 연도 경계를 그대로 쓰지 않도록 별도 처리했다. 새 자체 날짜 파서를 만들기 전에 사용 중인 HTTP 클라이언트의 지원 범위를 확인할 부분이다.
0초와 해석 실패에는 다른 정책을 붙인다
파서가 0을 반환했다고 재시도를 무한 반복할 이유는 없다. 반대로 None을 0으로 바꾸면 헤더 누락이나 잘못된 응답이 모두 즉시 재요청으로 이어진다. 파싱 결과와 실제 재시도 정책을 두 단계로 처리한다.
이번 실험에서는 다음 정책을 선택했다. HTTP 규격이 정한 기본값이 아니라 비교를 위한 애플리케이션 정책이다.
- 누락·해석 실패에는 2초를 사용한다.
- 유효한
0이나 과거 날짜에는 최소 1초를 둔다. - 최초 요청을 포함해 세 번째 시도까지 실패했으면 멈춘다.
- 대기 시간이 작업의 남은 시간 이상이면 더 요청하지 않고 실패를 반환한다.
다음은 그 판단만 확인하는 코드다. 앞선 파서에서 얻은 정수 또는 None이 delay에 들어간다고 가정한다. 같은 파일 내용을 이 블록으로 바꾸어 실행하면 된다. 실제로 기다리지는 않는다.
def wait_or_stop(delay, attempt, remaining):
if attempt >= 3:
return "stop"
wait = max(1, 2 if delay is None else delay)
return wait if wait < remaining else "stop"
for delay in (90, 0, None):
print(wait_or_stop(delay, attempt=1, remaining=60))
실제 출력은 차례로 stop, 1, 2였다. 서버가 90초 뒤를 가리키는데 작업에는 60초만 남았다면, 지연을 60초로 줄여 일찍 재요청하는 대신 이 작업을 끝내는 선택이다. 남은 시간이 120초인 조건에서는 90초가 선택됐고, 세 번째 실패 뒤에는 0이 들어와도 중단됐다. delay or 2로 작성하면 유효한 0도 2로 바뀌므로, 여기서는 is None으로 구분했다.
실제 호출에 붙일 때는 응답을 받을 때의 현재 시각을 파서에 전달하고, 전체 작업의 남은 시간과 다음 요청의 제한 시간을 함께 관리한다. 위 함수는 다음 요청을 시작할지 고르는 계산일 뿐 전체 호출 시간을 제한하는 실행기가 아니다. 또한 여기서 확인한 것은 다시 요청해도 되는 GET의 대기 판단이다. 상태를 변경하는 요청의 중복 실행 가능성은 별도로 판단해야 한다.
직접 확인한 범위는 합성 429·503 응답, 두 헤더 형식, 과거·미래·잘못된 값·누락·0과 고정 시계의 계산이다. 실험용 날짜 어댑터는 메일 날짜 라이브러리를 이용하므로 모든 HTTP 문법을 엄격하게 검사하는 구현은 아니며, 윤초와 중복 헤더 처리는 검증하지 않았다. 서버와 클라이언트의 시계 차이, 기존 클라이언트 라이브러리의 자동 재시도, 실제 대기 후 요청 간격도 측정하지 않았다. 날짜 계산값부터 맞는지 확인한 뒤 재시도 실행 경로를 연결해야, 날짜 해석 실패와 스케줄러 문제를 구별할 수 있다.