이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- HTTP message body가
Content-Length,Transfer-Encoding, multipart boundary로 어디까지인지 결정되는 원리를 설명할 수 있는가?- body size limit, proxy buffering, slow upload, streaming이 400/413/502/504로 이어지는 이유는 무엇인가?
- request smuggling, double read, chunked incompatibility 같은 드문 문제를 어떻게 예방하고 진단하는가?
개요
HTTP body는 “그냥 뒤에 붙은 데이터”가 아니다. 서버와 proxy는 framing 규칙을 보고 body가 어디서 시작하고 어디서 끝나는지 판단한다. 이 판단이 어긋나면 앱 코드에 도달하기 전 400/413이 나거나, 업로드가 멈추거나, request smuggling 같은 보안 문제가 생긴다.
- HTTP body는
Content-Length, chunked transfer, multipart boundary로 해석된다. - 파일 업로드와 streaming은 proxy buffer와 timeout을 함께 본다.
- 잘못된 framing은 애플리케이션 코드에 도달하기 전 실패할 수 있다.
왜 백엔드 개발자가 알아야 하는가
백엔드에서 파일 업로드, webhook, streaming, large JSON, multipart form을 다루면 body framing과 proxy 설정이 곧 API 품질이 된다. 이 문제는 앱 로그에 안 남는 경우가 많아 더 까다롭다.
- proxy의
client_max_body_size는 20MB인데 app은 50MB를 허용하면 사용자는 proxy 413을 본다. - chunked upload를 upstream이 제대로 처리하지 못하면 proxy 502나 400이 날 수 있다.
- 느린 업로드는 worker/thread/connection을 오래 점유해 다른 요청까지 밀리게 한다.
- body를 logging filter에서 먼저 읽어 버리면 controller에서 body가 비어 보일 수 있다.
Content-Length와Transfer-Encoding불일치는 request smuggling 방어 관점에서 중요하다.
요청 흐름에서의 위치
framing은 HTTP parser와 proxy가 application handler 전에 처리하는 영역이다.
client body
→ CDN/WAF size limit
→ reverse proxy buffering and timeout
→ app server HTTP parser
→ framework body reader
→ controller/service요청이 앱 로그에 없다면 CDN/WAF/proxy/body parser 단계에서 막혔을 수 있다. 반대로 앱에 도달했지만 controller body가 비어 있다면 framework/filter에서 body를 이미 소비했을 수 있다.
원리
HTTP/1.1에서 body 길이는 대표적으로 다음 방식으로 정해진다.
| 방식 | 의미 | 실무 포인트 |
|---|---|---|
Content-Length | byte 길이를 명시 | 실제 body와 다르면 hang/400 가능 |
Transfer-Encoding: chunked | chunk 단위로 끝을 표시 | proxy/upstream 호환성 확인 필요 |
| multipart boundary | part 경계를 boundary로 표시 | upload/form parsing 실패 가능 |
| connection close | 연결 종료로 끝 판단 | keep-alive와 충돌 가능 |
HTTP/2와 HTTP/3는 binary frame 기반이라 framing 방식이 다르지만, application이 느끼는 body 크기 제한, streaming, flow control 문제는 여전히 남는다.
핵심 판단 기준
- CDN/WAF/proxy/app의 body size limit을 가장 작은 값 기준으로 맞춘다.
- upload API는 request timeout, read timeout, buffering, temp disk 사용량을 함께 설계한다.
- streaming API는 proxy buffering을 꺼야 할 수 있다. 그렇지 않으면 client가 streaming처럼 받지 못한다.
- body logging은 size 제한과 masking을 반드시 둔다.
Content-Length와Transfer-Encoding이 동시에 이상하게 들어오는 요청은 보안 장비와 app server가 같은 정책으로 거절해야 한다.
실무에서 자주 만나는 문제
- body size 제한이 proxy와 app에서 다르다.
- chunked 요청을 upstream이 지원하지 않는다.
- 느린 업로드가 server connection을 오래 점유한다.
- multipart boundary 누락으로 framework가 body를 parse하지 못한다.
- CDN은 100MB를 허용하지만 app server temp disk가 부족해 실패한다.
- request body를 filter에서 읽고 다시 감싸지 않아 controller에서 body가 비어 있다.
- streaming response를 만들었는데 NGINX buffering 때문에 client가 끝까지 기다린다.
Expect: 100-continue를 proxy가 어색하게 처리해 upload 시작이 지연된다.
body 문제는 작은 요청에서는 정상이고 큰 요청이나 느린 네트워크에서만 실패한다. 테스트는 크기, 속도, content type, transfer 방식, proxy 경유 여부를 바꿔 가며 해야 한다.
디버깅 방법
- 실패 응답이 CDN/WAF/proxy/app 중 어디에서 생성되었는지 본다.
- request
Content-Length,Transfer-Encoding,Content-Type, multipart boundary를 확인한다. - proxy error log에서 “client intended to send too large body”, “upstream prematurely closed” 같은 메시지를 찾는다.
- app access log에 요청이 도달했는지, body read 시간이 긴지 본다.
- 큰 파일, 느린 업로드, chunked upload를 각각 재현한다.
# 큰 body size 제한 확인
dd if=/dev/zero of=/tmp/large.bin bs=1m count=25
curl -v -X POST https://api.example.com/upload \
-H 'Content-Type: application/octet-stream' \
--data-binary @/tmp/large.bin
# chunked 전송 재현
curl -v --http1.1 -X POST https://api.example.com/events \
-H 'Transfer-Encoding: chunked' \
--data-binary @/tmp/large.bin413이 proxy에서 나면 app 로그에 없을 수 있다. app 413인지 proxy 413인지 response header와 proxy log로 나눠 본다.
코드로 확인하기
proxy와 Spring의 body limit은 같이 맞춘다.
location /api/ {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 2s;
proxy_read_timeout 30s;
client_max_body_size 20m;
proxy_request_buffering on;
}# Spring Boot
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
server:
tomcat:
max-http-form-post-size: 20MB프록시 설정은 애플리케이션 코드 밖의 API 계약이다. body 크기, buffering, timeout, Host, forwarding header가 앱의 기대와 어긋나면 코드가 정상이어도 장애가 난다.
주니어가 자주 하는 오해
- body는 framework가 알아서 읽어 준다고만 생각한다.
- proxy와 app server가 body를 parse하기 전에 막을 수 있다.
- 413이면 항상 application이 거절한 것이라고 생각한다.
- CDN/WAF/proxy가 먼저 거절할 수 있다.
- streaming은 controller에서 flush만 하면 된다고 생각한다.
- proxy buffering과 client timeout이 streaming 체감을 바꾼다.
- request body를 로그에 남기면 디버깅이 쉬워진다고 생각한다.
- 민감정보와 큰 body로 인해 보안/비용/성능 문제가 생긴다.
시니어의 설계 판단 기준
- upload API는 최대 크기, timeout, temp storage, virus scan, retry 가능성, idempotency를 함께 설계한다.
- streaming API는 proxy buffering, heartbeat, client disconnect, backpressure를 설계에 넣는다.
- body size limit은 security와 UX의 절충이다. 크게 열수록 DoS 위험과 비용이 커진다.
- request smuggling 방어를 위해 proxy와 app server의 HTTP parser 정책을 일관되게 유지한다.
- 큰 body endpoint는 일반 API thread pool과 분리하거나 별도 경로를 둘 수 있다.
인프라 협업 포인트
- 인프라 팀과 CDN/WAF/proxy/app의 body size limit 표를 맞춘다.
- upload/streaming endpoint는 proxy buffering, temp file path, timeout, client_max_body_size를 별도 검토한다.
- “앱 로그가 없다”는 사실은 proxy/WAF 단계에서 body가 거절되었다는 단서가 될 수 있다.
- 인프라 담당자는 body limit을 무작정 키우면 비용, disk, DoS 위험이 커진다는 고충이 있다.
실전 팁
- 파일 업로드는 직접 앱으로 받기보다 object storage pre-signed URL을 고려한다.
- body limit은 문서와 error message에 드러내되 내부 인프라 구조는 노출하지 않는다.
- request body logging은 sample, size cap, masking을 둔다.
- streaming 응답은 운영에서 실제로 chunk가 flush되는지
curl --no-buffer로 확인한다.
위험 신호!
- CDN, WAF, proxy, app의 body size limit이 서로 다르지만 문서가 없다.
- upload endpoint가 일반 API와 같은 timeout/thread pool을 쓴다.
- streaming endpoint 앞에 proxy buffering이 켜져 있다.
- request body를 무제한으로 로그에 남긴다.
Content-Length와 실제 body size 불일치 요청을 어떻게 처리하는지 아무도 모른다.
확인 질문
확인 질문
- 413이 발생했을 때 app 로그가 없는 이유는 무엇인가?
- CDN/WAF/proxy가 body size limit으로 애플리케이션에 전달하기 전에 거절했을 수 있기 때문이다.
- streaming 응답이 한 번에 몰아서 도착하면 무엇을 의심해야 하는가?
- proxy buffering, compression buffering, framework flush 정책, client buffering을 의심해야 한다.
- upload API에서 body limit을 키울 때 함께 봐야 할 것은 무엇인가?
- proxy/app limit, timeout, temp storage, thread/connection 점유, DoS 위험, 사용자 에러 메시지를 함께 봐야 한다.