증상
승인 요청은 보낼 바디가 없는 PUT이다. 응답이 411 Length Required인데 본문이 HTML로 온다(<html><title>411 Length Required</title>). 이 API의 다른 응답은 전부 JSON이라 "중계가 깨졌나"로 먼저 읽힌다. 아니다.
원인
흔한 HTTP 클라이언트는 바디를 안 주면 Content-Length 헤더를 아예 안 붙인다. 게이트웨이는 길이 없는 PUT을 411로 끊는다. HTML은 게이트웨이의 에러 페이지가 중계를 거쳐 그대로 온 것이다. 중계가 바디를 벗긴 게 아니라 애초에 실을 바디가 없었다.
★ 바디가 없는 것과 길이를 안 밝히는 것은 다르다. HTTP는 전자를 허용하지만 게이트웨이는 후자를 거부한다.
해법
페이로드를 발명하지 않는다({}도 임의값이다). 빈 바디 + 길이 0을 명시한다.
body = b''
headers['Content-Length'] = '0'
(요청 서명이 method·path·date만 쓴다면 바디를 바꿔도 서명은 불변이다 — 이 수정은 인증을 건드리지 않는다.)
관문이 둘이다
클라이언트만 고쳐도 안 끝났다. 요청이 고정 IP 중계를 거쳐 나가므로 관문이 하나 더 있다. 중계가 빈 바디일 때 curl의 POSTFIELDS를 조건부로 세팅하고 있었다.
if ($body !== null && $body !== '') curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
읽으면 오히려 방어적으로 보인다 — "빈 바디를 굳이 세팅할 이유가 있나?" 그게 함정이다. 빈 바디를 세팅하는 것이 곧 길이 0을 선언하는 것이라 여기선 조건이 곧 결함이다. 바디가 비어도 항상 세팅하고, 호출부가 준 Content-Length는 버리고 실제 바이트 수로 다시 계산한다(어긋나면 게이트웨이가 끊는다).
★ 경유가 있으면 관문도 그만큼 늘어난다. 우리 쪽을 고쳐도 중계가 흘리면 그대로다. 첫 수리의 mock이 직결 경로만 검사해서 초록이 떴다 — 그래서 이 사실을 늦게 알았다.
파급
411 하나가 네 겹으로 번졌다: 승인요청이 안 걸림 → 항목이 초안에 머묾 → 초안은 사유 텍스트가 없어 감시 분류에서 "미분류"로 떨어짐 → 대시보드 카드가 그걸 못 보여줌(앞 항목의 ①). 화면 어디에도 안 뜨는 채로 이틀.
검증
★ 오류가 다른 층으로 옮겨 가는 것(411 → 401)이 수리의 증거다. 중계 배포 뒤 같은 호출이 411 대신 401을 냈다. 411이 사라졌다는 건 curl이 길이를 붙였다는 뜻이고, 남은 401은 인증 층의 문제다. 전송 계층이 끝났으니 다음 층으로 갈 수 있다. 조용해지는 것보다 옮겨 가는 것이 낫다. 그 뒤 success: true, 그 뒤 심사 큐 진입 — 각 단계가 다음 단계의 증거다.