요청 취소의 경계

HTTP 요청 취소는 클라이언트에서만 일어난다.1 이미 보낸 바이트를 회수하지도, 서버가 하고 있는 일을 멈추지도 못한다. 취소한 순간에 요청 전송이 이미 끝나 있었나. 끝나 있었다면 서버는 그걸 받았고, 받았으면 처리한다. 취소는 그 처리를 못 막는다.

구간 어디 여기서 취소하면
send 클라이언트 요청이 다 전송되지 않는다 — 헤더·본문 일부는 도착할 수 있다
in-flight 네트워크 이미 다 보냈다. 서버는 받는다
processing 서버 서버가 처리 중이고 멈출 방법이 없다
resp 네트워크 → 클라이언트 응답이 오는 중이다. 클라이언트가 안 읽을 뿐

구간 사이 경계는 셋인데 결과를 가르는 건 첫 번째 하나다 — send와 in-flight 사이, 전송이 끝나는 순간. 그 뒤로는 어디서 취소하든 서버 입장에선 똑같다.

요청은 send(클라이언트) → in-flight(네트워크) → processing(서버) → resp(응답) 네 구간을 지난다. 결과를 가르는 경계는 send 가 끝나는 순간 하나다. 빠른 네트워크에서 전송이 끝난 뒤 abort 하면 promise 는 reject 되지만 서버는 요청을 받아 처리하고, 클라이언트는 응답을 안 읽을 뿐이다. 같은 시각에 abort 해도 네트워크가 느려 전송이 그 시각을 넘겨 끝나면 전송이 끊겨 서버는 요청을 다 받지 못한다(헤더·본문 일부는 도착할 수 있다). 서버가 처리 중일 때 abort 해도 서버 입장에선 전송 뒤 abort 와 같다 — reject 이면서 서버는 처리한다.

클라이언트에서는 네 경우가 똑같이 보인다 — promise는 AbortError, CDP는 Network.loadingFailed(canceled: true, net::ERR_ABORTED). 응답 전에 끊기면 responseReceived도 안 와서 timing.sendEnd조차 없다. “서버가 받았나”의 정답은 서버 로그에만 있고, CDP는 취소를 원하는 구간에 정확히 일으키는 데 쓴다.2

구간 CDP로 일으키기 서버 로그
send 전 Fetch.enable({ patterns: [{ requestStage: 'Request' }] })로 보내기 전에 붙잡고 abort 없음
send 중 Network.emulateNetworkConditions의 uploadThroughput을 낮추고(200KB 본문, 20KB/s) abort 헤더 + 본문 16KB, 미완료, 핸들러 안 돎
processing 서버가 오래 걸리는 엔드포인트에서 abort 받음, 처리 완료
resp requestStage: 'Response'로 응답을 붙잡고 abort 받음, 처리 완료

send 중 줄이 경계를 한 번 더 쪼갠다. 전송 도중 취소하면 서버는 아무것도 못 받는 게 아니라 헤더와 본문 일부를 받는다. 본문이 끝나야(end) 처리하는 서버라 핸들러가 안 돌았을 뿐이다. 헤더만 보고 일을 시작하는 서버라면 전송 완료 전 취소도 부수효과를 낸다 — “경계는 전송 완료”는 본문까지 받고 처리하는 서버에서의 이야기다.

Footnotes

  1. 표준은 연결을 끊으라고(HTTP/2면 RST_STREAM, 아니면 성능에 나쁘지 않으면 닫으라고) 할 뿐, 서버가 받은 것을 되돌리는 단계는 없다. Fetch Standard — HTTP-network fetch: “If connection uses HTTP/2, then transmit an RST_STREAM frame. Otherwise, the user agent should close connection unless it would be bad for performance to do so.” ↩

  2. Chrome 154 headless 에서 raw CDP 로 실측 (2026-10-01). 로컬 Node 서버가 요청 수신·본문 바이트·end·응답 완료를 기록했고, 페이지는 AbortController 로 POST 를 취소했다. 네 경우 모두 promise AbortError, Network.loadingFailed canceled: true net::ERR_ABORTED 로 같았다. uploadThroughput 스로틀링은 localhost 요청에도 걸렸다. ↩

#572

CORS error 원인 찾기

언제 왜
Console·Network 에 CORS error preflight 에 Access-Control-Allow-Origin(ACAO) 이 없으면 브라우저는 5xx·404·타임아웃을 전부 CORS 로 뭉갠다1
preflight 가 server: awselb/2.0 의 5xx LB 가 직접 만든 응답이다 — 앱 응답이 아니니 CORS 헤더가 붙을 수 없다

정상이면 OPTIONS 가 LB 를 지나 앱에 닿고 앱이 204 와 access-control-allow-origin 을 돌려준다. 장애면 등록된 타깃이 0 이라 LB 가 CORS 헤더 없는 503 을 직접 만들어 돌려주고, 브라우저는 그걸 CORS error 로 표시한다.

preflight 의 status 와 server 헤더 둘로 가른다.

preflight server ACAO 갈래 원인
503 awselb/2.0 없음 인프라 등록된 target 02 — 서비스 다운·배포 실패·CrashLoop
502 awselb/2.0 없음 인프라 타깃이 끊거나 잘못 응답 — 크래시·OOM·포트 불일치
504 awselb/2.0 없음 인프라 LB idle timeout 초과 — 느린 쿼리·외부 연동 지연
404 앱 없음 경로 path 오타, ingress rule 불일치
204·200 앱 없음 CORS CORS 미설정
204 앱 있음 CORS access-control-allow-headers 에 요청 헤더가 빠짐
204 앱 * CORS credentials: 'include' 와 * 는 병용 불가 — 오리진을 명시한다
204 앱 정상 정상 CORS 아님. 본 요청의 본문·인증·에러를 본다

Footnotes

  1. CORS errors — MDN: “For security reasons, specifics about what went wrong with a CORS request are not available to JavaScript code. All the code knows is that an error occurred.” ↩

  2. ALB 트러블슈팅 — HTTP 503: “The target groups for the load balancer have no registered targets, or all of the registered targets are in an unused state.” 전부 unhealthy 면 503 이 아니라 그대로 보낸다 — health checks: “If a target group contains only unhealthy registered targets, the load balancer routes requests to all those targets, regardless of their health status.” ↩

#599