Published on

Nginx 413 Request Entity Too Large 업로드 실패 해결

Authors

서버에 파일 업로드를 붙이다 보면 어느 순간 프론트에서는 업로드가 멈추고, 브라우저 네트워크 탭에는 413 Request Entity Too Large가 찍히는 상황을 만나게 됩니다. 이 오류는 “애플리케이션이 파일을 처리하지 못했다”가 아니라, Nginx(또는 그 앞단 프록시)가 요청 바디 자체를 너무 크다고 판단해 업스트림으로 전달하기 전에 차단했다는 뜻입니다.

문제는 설정이 한 군데만 있는 게 아니라는 점입니다. 로컬 Nginx, Docker, Kubernetes Ingress, Cloud LB, 그리고 앱 서버(예: Gunicorn/Uvicorn)까지 어디에서든 바디 제한/버퍼링이 걸릴 수 있어 “올렸는데도 413이 계속” 같은 일이 흔합니다. 이 글에서는 413을 재현 → 원인 지점 식별 → 계층별 설정 일치 → 검증 순서로 확실히 끝내는 실전 체크리스트를 제공합니다.

413의 의미와 발생 위치

HTTP 413이 나는 대표 지점

  1. Nginx 리버스 프록시: client_max_body_size 기본값(또는 배포 기본값)이 낮아 업로드 차단
  2. Kubernetes NGINX Ingress Controller: Ingress annotation으로 제한이 걸림
  3. 클라우드 로드밸런서/프록시: 일부 환경은 요청 크기 제한이 별도로 존재
  4. 애플리케이션/프레임워크: 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 /apilocation /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/앱 제한이 겹쳐 가장 작은 제한이 계속 발목을 잡습니다.

  1. 에러 로그로 413의 발생 지점을 확정하고, 2) 업로드 경로에만 필요한 만큼 허용하며, 3) Ingress/Nginx/앱의 제한을 일관되게 맞춘 뒤, 4) curl로 재현/검증까지 하면 같은 문제가 재발해도 빠르게 복구할 수 있습니다.