- Published on
Nginx 413 Request Entity Too Large 업로드 실패 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버에 파일 업로드를 붙이다 보면 어느 순간 프론트에서는 업로드가 멈추고, 브라우저 네트워크 탭에는 413 Request Entity Too Large가 찍히는 상황을 만나게 됩니다. 이 오류는 “애플리케이션이 파일을 처리하지 못했다”가 아니라, Nginx(또는 그 앞단 프록시)가 요청 바디 자체를 너무 크다고 판단해 업스트림으로 전달하기 전에 차단했다는 뜻입니다.
문제는 설정이 한 군데만 있는 게 아니라는 점입니다. 로컬 Nginx, Docker, Kubernetes Ingress, Cloud LB, 그리고 앱 서버(예: Gunicorn/Uvicorn)까지 어디에서든 바디 제한/버퍼링이 걸릴 수 있어 “올렸는데도 413이 계속” 같은 일이 흔합니다. 이 글에서는 413을 재현 → 원인 지점 식별 → 계층별 설정 일치 → 검증 순서로 확실히 끝내는 실전 체크리스트를 제공합니다.
413의 의미와 발생 위치
HTTP 413이 나는 대표 지점
- Nginx 리버스 프록시:
client_max_body_size기본값(또는 배포 기본값)이 낮아 업로드 차단 - Kubernetes NGINX Ingress Controller: Ingress annotation으로 제한이 걸림
- 클라우드 로드밸런서/프록시: 일부 환경은 요청 크기 제한이 별도로 존재
- 애플리케이션/프레임워크: FastAPI/Starlette, Django, Spring 등에서 별도 제한
Nginx에서 413이 나면 보통 업스트림 로그에는 아무 것도 남지 않습니다. 왜냐하면 요청이 앱까지 도달하지 않았기 때문입니다.
빠른 확인: 에러 로그에서 “누가 413을 냈는지”
Nginx가 413을 반환하면 에러 로그에 유사한 메시지가 남습니다.
sudo tail -n 200 /var/log/nginx/error.log
대표적으로 다음과 같은 문구가 보입니다.
client intended to send too large body: ...
이 문구가 있으면 Nginx 단계에서 차단된 것입니다.
가장 흔한 원인: client_max_body_size
Nginx에서 요청 바디(업로드 포함) 허용 크기를 결정하는 핵심 설정은 client_max_body_size입니다.
- 적용 범위:
http/server/location블록 - 우선순위: 더 좁은 범위(location)가 server/http를 덮어씀
해결 1) server 전체에 적용
예: 업로드 최대 50MB 허용
server {
listen 80;
server_name example.com;
client_max_body_size 50m;
location / {
proxy_pass http://app;
}
}
해결 2) 특정 업로드 경로에만 적용
업로드 API만 크게 열고 나머지는 보수적으로 유지하고 싶다면 다음처럼 분리합니다.
server {
listen 80;
server_name example.com;
client_max_body_size 2m; # 기본은 작게
location /api/upload {
client_max_body_size 50m;
proxy_pass http://app;
}
location / {
proxy_pass http://app;
}
}
설정 반영 및 문법 검증
sudo nginx -t
sudo systemctl reload nginx
restart대신reload를 권장(무중단)nginx -t가 실패하면 반영되지 않습니다
“올렸는데도 413”이 계속되는 5가지 함정
1) location 매칭 때문에 다른 블록이 적용됨
location /api와 location /api/upload가 섞여 있으면 정확히 어느 location이 매칭되는지가 중요합니다.
안전한 패턴은 더 구체적인 location을 먼저 정의하거나, 정규식 location을 사용할 때 우선순위를 명확히 하는 것입니다.
location = /api/upload { # 완전 일치
client_max_body_size 50m;
proxy_pass http://app;
}
location /api/ {
client_max_body_size 2m;
proxy_pass http://app;
}
2) reverse proxy가 여러 겹(ELB → Ingress → Nginx → App)
요청이 통과하는 모든 계층에서 제한이 다르면, 가장 작은 제한이 병목이 됩니다. Kubernetes 환경이라면 특히 Ingress를 먼저 의심해야 합니다. Ingress/스트리밍/타임아웃 튜닝은 아래 글도 함께 참고하면 디버깅이 빨라집니다.
3) Kubernetes NGINX Ingress에서 body size 제한
NGINX Ingress Controller는 annotation으로 바디 제한을 제어합니다. (컨트롤러/차트 버전에 따라 키가 다를 수 있으나 보통 아래가 표준적으로 쓰입니다.)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
spec:
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-svc
port:
number: 80
적용 후에는 Ingress Controller가 생성한 Nginx 설정이 갱신되므로, 컨트롤러 파드 로그/이벤트를 함께 확인하세요.
4) 앱/프레임워크의 업로드 제한(413이 아니라 400/422로 보일 수도)
Nginx를 통과해도 앱에서 제한을 두면 업로드가 실패합니다. 이때는 413이 아니라 다른 코드로 보이기도 합니다.
- FastAPI/Starlette는 일반적으로 Nginx에서 먼저 막히는 경우가 많지만, 멀티파트 파서/리버스 프록시 구성에 따라 애플리케이션 레벨에서 에러가 날 수 있습니다.
- 업로드를 “요청 한 번에 크게” 보내는 대신, 청크/분할 업로드로 설계를 바꾸는 게 장기적으로 안정적입니다.
대용량 업로드를 API 설계로 풀어야 한다면, 413을 단순 설정으로만 해결하지 말고 청크 전략까지 같이 검토하는 편이 좋습니다.
5) proxy_request_buffering / temp file 동작(대용량에서 체감 장애)
413 자체와 별개로, 대용량 업로드는 Nginx가 요청 바디를 버퍼링하면서 디스크에 임시 파일을 만들 수 있고, 이 과정에서 성능 저하/지연/타임아웃이 동반될 수 있습니다.
일반적인 업로드 API라면 기본값으로도 충분한 경우가 많지만, 실시간 처리/스트리밍 성격이 강하거나 업스트림이 바로 읽어야 한다면 다음을 검토합니다.
location /api/upload {
client_max_body_size 200m;
# 업스트림으로 바로 흘려보내고 싶을 때(환경에 따라 신중히)
proxy_request_buffering off;
proxy_pass http://app;
}
proxy_request_buffering off는 메모리/커넥션 점유 패턴을 바꿀 수 있어, 트래픽이 많다면 부하 테스트 후 적용하세요.
재현과 검증: curl로 확실히 확인하기
브라우저로 테스트하면 CORS/프론트 로직이 섞여 원인 파악이 느립니다. 먼저 curl로 “순수 업로드 요청”이 통과하는지 확인하세요.
(1) 큰 파일 생성
# 60MB 더미 파일 생성
dd if=/dev/zero of=big.bin bs=1m count=60
(2) multipart 업로드 테스트
curl -v \
-F "file=@big.bin" \
https://example.com/api/upload
- 응답이 413이면 여전히 프록시 계층에서 막히는 중
- 응답이 2xx인데 앱에서 처리 실패라면 앱 로그를 확인
(3) 헤더/응답 주체 확인 팁
응답 헤더의 Server: nginx 여부, 혹은 Ingress가 붙인 헤더를 보면 “어느 계층이 응답했는지” 감이 옵니다.
curl -I https://example.com/api/upload
운영에서 권장하는 설정 기준
1) 제한값을 “요구사항 + 여유”로 정하고 문서화
- 예: 프로필 이미지 5MB, 첨부파일 50MB, 동영상 500MB
- 경로별로
location을 나누고 제한을 다르게 둡니다.
2) 계층별 제한을 동일하게 맞추기
- Ingress
proxy-body-size - Nginx
client_max_body_size - 앱 서버/프레임워크 제한
- (가능하면) 프론트에서도 사전 검사
이 중 하나라도 더 작으면 결국 그 값이 시스템의 실질 업로드 한계가 됩니다.
3) 관측 가능성(로그) 강화
Nginx access log에 요청 크기를 남기면 “어떤 요청이 컸는지”가 빠르게 보입니다.
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'request_length=$request_length '
'content_length=$http_content_length '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
$request_length: 요청 라인+헤더+바디 포함 총 길이$http_content_length: 클라이언트가 보낸 Content-Length
결론: 413은 “설정 1줄”이 아니라 “경로/계층 일치” 문제
client_max_body_size를 올리는 것만으로 끝나는 경우도 많지만, 운영 환경에서는 Ingress/LB/앱 제한이 겹쳐 가장 작은 제한이 계속 발목을 잡습니다.
- 에러 로그로 413의 발생 지점을 확정하고, 2) 업로드 경로에만 필요한 만큼 허용하며, 3) Ingress/Nginx/앱의 제한을 일관되게 맞춘 뒤, 4) curl로 재현/검증까지 하면 같은 문제가 재발해도 빠르게 복구할 수 있습니다.