Retry-After 날짜값에서 재시도가 멈춘 이유와 파서 수정
WordPress 발행기의 재시도 코드를 검토하면서 429 응답의 Retry-After를 숫자로만 바꾸는 한 줄을 발견했다. HTTP 규격은 초 단위 숫자뿐 아니라 날짜도 허용한다. 날짜형 응답을 테스트에 넣자 재시도는커녕 ValueError에서 바로 멈췄다. 검증 범위는 고정한 HTTPError fixture와 파서 회귀 테스트이며, 기존 코드의 빈 테스트 경로를 같은 입력으로 재현하고 고친 기록이다.
막힌 장면: 429를 처리하다 다른 예외가 났다
기존 구현은 429와 5xx를 재시도 대상으로 분류한 뒤 헤더가 있으면 곧바로 float()를 호출했다.
retry_after = exc.headers.get("Retry-After")
delay = float(retry_after) if retry_after else float(2**attempt)
초 단위 값인 7은 문제없지만 HTTP-date를 넣으면 요청 재시도 오류인 WordPressError가 아니라 파서의 ValueError가 밖으로 새었다.
$ .venv/bin/python - <<'PY'
print(float('Sat, 05 Sep 2026 00:00:07 GMT'))
PY
ValueError: could not convert string to float: 'Sat, 05 Sep 2026 00:00:07 GMT'
exit status: 1
확인 환경은 macOS Darwin 23.0.0 x86_64, Python 3.12.9다. 외부 WordPress에 일부러 429를 보내지 않고 HTTPError fixture로 응답을 고정했다. 재현 과정에서 글·미디어·설정은 변경하지 않았다.
기존 첫 접근이 놓친 지점
기존 첫 접근은 헤더가 있으면 숫자라고 가정해 float()로 읽는 것이었다. 500 <= code < 600이 501처럼 같은 요청으로 회복되기 어려운 상태도 포함하는 문제는 별도로 보였지만, 이번 fixture는 429 분기를 정상적으로 탄 다음 대기 시간을 읽는 순간 실패했다. 상태 코드 범위를 좁혀도 날짜 파싱 실패는 해결되지 않는다.
또 Retry-After가 항상 정수라고 가정하면 이 테스트를 건너뛸 수 있다. HTTP Semantics의 Retry-After 정의는 delay-seconds와 HTTP-date 두 형식을 모두 허용한다. 문서와 코드의 차이가 원인이었다.
수정: 숫자와 날짜를 한 함수에서 해석한다
수정은 세 가지 경우를 분리했다.
@staticmethod
def _retry_after_seconds(value: str | None, *, now: datetime | None = None) -> float | None:
if not value:
return None
try:
delay = float(value)
except ValueError:
try:
retry_at = parsedate_to_datetime(value)
except (TypeError, ValueError, OverflowError):
return None
if retry_at.tzinfo is None:
retry_at = retry_at.replace(tzinfo=UTC)
current = now or datetime.now(UTC)
delay = (retry_at - current).total_seconds()
return max(0.0, delay)
- 숫자는 기존처럼 초로 사용한다.
- 날짜는 UTC 기준 현재 시각과의 차이로 바꾼다.
- 둘 다 해석할 수 없으면 예외를 흘리지 않고 기존 지수 백오프로 돌아간다.
30초 상한은 그대로 유지했다. 서버가 더 긴 대기를 요구할 때 일찍 재요청하는 기존 정책까지 이번 수정에 섞으면 변경 범위와 운영 판단이 달라지기 때문이다. 이 상한은 후속 과제로 남겼다.
같은 입력으로 전후를 비교했다
수정 전후 모두 Sat, 05 Sep 2026 00:00:07 GMT, 기준 시각 2026-09-05T00:00:00Z를 사용했다.
수정 전 숫자 전용 식: ValueError
수정 후: delay=7.0초
잘못된 헤더 수정 후: 1.0초 지수 백오프로 fallback, 다음 응답 {"id": 50}
회귀 테스트는 날짜 계산과 잘못된 헤더의 fallback을 따로 고정했다.
$ .venv/bin/python -m unittest tests.test_wordpress_retry -v
test_legacy_numeric_only_parse_rejects_http_date ... ok
test_malformed_retry_after_falls_back_instead_of_raising_value_error ... ok
test_retry_after_http_date_is_converted_to_seconds ... ok
Ran 3 tests
OK
exit status: 0
구현은 publisher/wordpress.py, 재현 fixture는 tests/test_wordpress_retry.py에 남겼다. 두 링크는 2026년 9월 5일 비로그인 HTTP 200으로 확인한 고정 커밋을 가리킨다.
이 수정이 해결하지 않는 것
이 파서는 대기 시간을 안전하게 읽을 뿐, 모든 재시도를 안전하게 만들지는 않는다. 특히 글·미디어를 만드는 POST는 서버가 작업을 끝낸 뒤 응답만 유실되면 같은 요청으로 중복 리소스가 생길 수 있다. 고유 slug 조회, 생성 ID 저장, 서버 측 멱등성 계약이 없는 POST에는 상태 코드만 보고 자동 재시도를 확대하면 안 된다.
또 30초 상한과 지터 부재는 남아 있다. 여러 발행기가 동시에 제한되면 같은 시각에 다시 몰릴 수 있다. 이번 변경의 채택 기준은 좁다. HTTP-date나 잘못된 헤더가 파서 예외로 재시도 흐름을 끊지 않는 것까지다.
독자가 재현하려면
저장소를 받은 뒤 아래 테스트만 실행하면 외부 API나 자격 증명 없이 같은 분기를 확인할 수 있다.
.venv/bin/python -m unittest tests.test_wordpress_retry -v
운영 환경에서 시험할 때는 실제 글 생성 POST 대신 GET 또는 mock 서버부터 사용해야 한다. 이 테스트가 통과해도 네트워크 장애와 중복 발행 안전성까지 증명하는 것은 아니다.
Retry-After 값을 계산하기 — 입력값만 검사하는 브라우저 도구이며 실제 사이트에 요청하지 않습니다.