REST API 발행 / VERIFIED CASE

HTTP 200인데 WordPress REST 발행이 실패한 이유

자동발행 검사를 만들다가 HTTP 200인데 본문이 로그인 HTML인 응답을 넣어봤다. 상태 코드만 보는 검사는 성공이라고 답했다. Content-Type과 생성된 글 ID까지 확인하도록 바꾸자 같은 입력이 unexpected_content_type으로 막혔다. 정상 JSON fixture는 같은 검사에서 종료 코드 0으로 통과했다. 이 글은 실제 운영 장애 회고가 아니라, 그 오판 조건을 격리된 fixture에서 만든 통제 실험이다.

실패 조건을 먼저 고정했다

입력은 세 가지다. 상태는 200, Content-Typetext/html, 본문은 로그인 페이지 형태의 HTML이다. 운영 WordPress에는 요청하지 않았다.

.venv/bin/python scripts/huntlab_wp_diagnostics.py rest-response \
  --status 200 \
  --content-type 'text/html; charset=UTF-8' \
  --body evidence/lab-fixtures/rest-html-200.html

상태 코드만 보는 이전 판단은 이 입력을 성공으로 통과시킨다. 강화한 진단기는 다음처럼 종료 코드 1과 구체적인 이유를 돌려줬다.

passed=false
reason=unexpected_content_type
status=200
content_type=text/html
body_bytes=99

핵심은 “200이 아니면 실패”가 아니라 “이 API 호출에서 기대한 응답 계약인가”다.

처음에는 허용 상태 코드 목록만 더 촘촘하게 만들 생각이었다. 하지만 HTML 로그인 페이지도 200을 쓸 수 있으므로 그 방법은 문제를 해결하지 못한다. 그래서 상태 코드 규칙은 유지하되, 응답 형식과 생성된 대상의 ID를 별도 조건으로 추가했다.

생성 요청의 성공 계약

게시물 생성 호출이라면 최소한 다음 세 조건을 함께 확인해야 한다.

  1. 허용한 HTTP 상태인가.
  2. 응답의 media type이 application/json인가.
  3. JSON을 파싱했을 때 생성된 게시물의 정수 ID가 기대값과 일치하는가.

진단 코드의 결정 지점은 다음처럼 작다.

media_type = content_type.split(";", 1)[0].strip().lower()
if media_type != "application/json":
    return failed("unexpected_content_type")

payload = json.loads(body)
if payload.get("id") != expected_id:
    return failed("post_identity_mismatch")

전체 구현과 fixture는 고정 커밋의 진단 스크립트테스트에서 확인할 수 있다.

같은 검사기에 정상 응답을 넣었다

비교 입력은 201, JSON content type, id=742인 최소 응답이다.

.venv/bin/python scripts/huntlab_wp_diagnostics.py rest-response \
  --status 201 \
  --content-type 'application/json; charset=UTF-8' \
  --body evidence/lab-fixtures/rest-post-201.json \
  --expected-id 742

결과는 종료 코드 0이었다.

passed=true
reason=validated_json_post_identity
status=201
content_type=application/json
post_id=742

실패와 성공은 같은 Python 3.12.9 환경, 같은 검사기, 고정된 입력 파일로 비교했다. 이 실험 중 네트워크 쓰기와 WordPress 쓰기는 각각 0회였다.

회귀 테스트가 막는 오판

test_html_login_page_with_200_exposes_status_only_false_positive는 200 HTML이 거절되는지 검사한다. test_json_post_identity_passes_same_response_contract는 JSON 응답의 ID까지 일치할 때만 통과하는지 검사한다. 네 개 진단 회귀 테스트를 실행한 결과는 모두 통과였다.

.venv/bin/python -m unittest tests.test_huntlab_wp_diagnostics -v

검사 결과와 입력 파일 SHA는 Evidence Lab 실행기가 기록한다. 따라서 나중에 fixture가 바뀌면 같은 결과라고 주장할 수 없다.

이 검사를 적용하면 안 되는 경우

읽기 API는 정상 상태가 200일 수 있고, 삭제 API는 204처럼 본문이 없는 성공을 사용할 수 있다. 모든 REST 호출에 201과 id를 강제하면 또 다른 오판이 된다. 호출 종류별로 허용 상태와 필수 응답 필드를 선언해야 한다.

또 이 fixture는 프록시 제품 하나의 실제 동작을 재현한 것이 아니다. 확인한 범위는 “HTML 200을 상태 코드만으로 성공 처리하는 클라이언트 결함”이다. 특정 플러그인이나 호스팅 서비스가 원인이라고 확대하지 않는다.

발행 후 검증 체크리스트

  • 요청 종류별 허용 상태를 분리한다.
  • 응답 media type을 파싱한 뒤 JSON만 디코딩한다.
  • 생성·수정 대상의 ID와 slug를 read-back으로 대조한다.
  • JSON 파싱 실패를 성공으로 기록하지 않는다.
  • 실패 시 재시도 가능 여부와 중복 생성 위험을 별도로 판단한다.

복사해서 실행하기

저장소를 받은 뒤 아래 한 줄로 실패 fixture와 정상 fixture, 회귀 테스트를 함께 확인할 수 있다.

.venv/bin/python scripts/run_evidence_lab.py rest-html-200-response \
  --output /tmp/rest-html-200-response.json

예상 결과는 before.exit_code=1, after.exit_code=0, status=READY, wordpress_writes=0이다. 값이 다르면 글의 결론을 그대로 적용하지 않는다.

진단 스크립트와 재현 fixture 보기