요청 취소의 경계

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. Fetch 표준의 abort 알고리즘은 controller 상태를 aborted로 바꾸고 이유를 기록할 뿐, 서버로 무엇을 보내라고 정하지 않는다. 연결을 닫거나 응답 읽기를 멈추는 건 구현 재량이다. (Fetch Standard — abort a fetch) ↩

  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