업로드 도중 413을 먼저 보내도 브라우저는 파일을 끝까지 보낸다, Connection close 의 엔진별 차이
본문을 받기 전에 413 으로 거절해도 대부분의 브라우저는 100MB 를 끝까지 보냅니다. Connection close 를 붙이면 전송은 줄지만 Firefox 는 413 대신 네트워크 오류를 냅니다.
용량 한도가 있는 파일 업로드 API에 조기 거부를 넣었습니다. 본문(multipart)을 다 받기 전에 Content-Length만 보고 한도를 확실히 넘으면 413을 먼저 돌려주는 방식입니다. 원래는 업로드 미들웨어(multer)가 파일을 스토리지까지 다 올린 뒤에야 거절해서, 그 비용을 없애려는 것이었습니다.
기대는 이랬습니다. 큰 파일을 끝까지 받지 않으니 전송량과 시간이 줄겠죠?
413을 먼저 보내면 브라우저는 멈출까
궁금한 건 두 가지였습니다. 이미 날아오고 있는 본문을 서버가 어떻게 처리하는지, 그리고 브라우저가 413을 제대로 받는지.
Node 24 HTTP 서버(Express)가 본문을 읽지 않고 곧바로 res.statusCode = 413; res.end(...) 하게 만들었습니다. 여기에 브라우저 XHR로 100MB FormData를 올렸고, Playwright로 세 엔진을 모두 쟀습니다.
| 엔진 | 연결 유지(기본) | Connection: close 추가 |
|---|---|---|
| Chromium | 100MB 다 보낸 뒤 413 | 0.8MB 에서 413 |
| WebKit | 약 1MB 에서 멈추고 413 | 0MB 에서 413 |
| Firefox | 100MB 다 보낸 뒤 413 | 413 대신 네트워크 오류 |
비교용으로 curl도 돌려 봤습니다. curl은 조기 응답을 받으면 바로 전송을 멈춥니다(0.5MB). Node의 http.request 클라이언트는 서버가 읽지 않으니 backpressure로 멈춰 섭니다.
왜 연결을 유지하면 다 보낼까
연결을 유지하면 Node는 응답을 보낸 뒤에도 남은 요청 본문을 읽어서 버립니다. keep-alive 연결을 재사용하려면 본문 경계를 끝까지 읽어야 하기 때문입니다.
그리고 대부분의 브라우저는 요청 본문을 다 보낸 뒤에야 응답을 읽습니다. 서버가 413을 일찍 보내도 클라이언트는 100MB를 끝까지 올리는 거죠. 그래도 세 엔진 모두 413은 정확히 받습니다.
Connection: close 를 붙이면
연결을 끊으면 전송량은 확 줄어듭니다. Chromium과 WebKit은 413을 제대로 받습니다.
문제는 Firefox입니다. 업로드 도중 끊긴 연결을 네트워크 오류로 처리해서, 앱이 용량 초과라는 이유를 사용자에게 보여 줄 수 없습니다. 응답 코드로 안내 문구를 고르는 클라이언트라면 사용자는 용량 부족 대신 업로드 실패를 보게 됩니다.
참고로 Expect: 100-continue는 서버가 본문 전에 거부할 수 있는 HTTP 표준 장치지만, 브라우저 fetch와 XHR은 이 헤더를 보내지 않습니다.
그래서 조기 413으로 뭘 얻나
조기 거부로 줄어드는 건 전송량이 아니라 서버 뒤쪽 작업입니다. 스토리지 업로드와 삭제, DB 쓰기가 0회가 되고 응답도 정확합니다.
전송량까지 줄이고 싶다면 연결을 끊는 대신 다른 층에서 풉니다. 첫 413을 받은 클라이언트가 남은 대기 파일 중 확실히 안 들어가는 것만 건너뛰게 하거나, 업로드 전에 용량을 조회해 미리 막는 방식입니다. presigned 방식이라면 발급 단계에서 거부하면 됩니다.
프록시(CDN, 로드밸런서)가 앞에 있으면 동작이 또 달라질 수 있습니다. 실제 경로에서 한 번 더 재 봐야 합니다.