413 에러가 뜻하는 것

413 Payload Too Large(RFC 9110에서는 413 Content Too Large로 이름이 바뀌었고, nginx 등 여전히 많은 서버가 413 Request Entity Too Large로 표시합니다)의 뜻은 하나입니다. 클라이언트와 내 코드 사이의 무언가가 요청 본문의 크기를 재 보고, 받아 줄 수 있는 크기보다 크다고 판단했습니다.

데이터도, 로그인도, URL도 문제가 아닙니다. 크기 한도이고, 진짜 질문은 어느 한도냐입니다. 업로드 한 건은 보통 CDN, 경우에 따라 로드밸런서, 리버스 프록시, 언어 런타임, 프레임워크를 차례로 지납니다. 층마다 본문 한도가 따로 설정돼 있고, 가장 먼저 “안 돼”라고 말한 층이 오류를 냅니다. 앞단의 nginx가 이미 거부했다면 앱에서 한도를 올려도 소용없습니다.

그래서 할 일은 세 가지입니다. 깨지는 크기를 찾고, 응답한 층을 찾고, 맞는 설정을 바꿉니다.

남의 사이트에 업로드하는 중이라면

그 사이트가 받는 크기보다 파일이 큽니다. 그쪽 한도는 바꿀 수 없으니 파일을 줄이세요. 사진은 크기를 줄이거나 다시 압축하고, 영상은 낮은 해상도로 내보내고, 큰 압축 파일은 나누면 됩니다. 사이트 도움말에 최대 크기가 적혀 있는지도 보세요. 작은 파일조차 실패한다면 사이트 설정이 잘못된 것이니 운영자에게 알려야 합니다.

1단계: 깨지는 크기 찾기

임계값이 대개 범인을 가리키므로 직접 재 보세요. 크기가 정해진 테스트 파일을 만들고, 실제 업로드와 같은 방식으로 보냅니다.

for size in 900K 2M 10M 50M 120M; do
  head -c $size /dev/urandom > /tmp/test-$size.bin
  printf '%s ' $size
  curl -s -o /dev/null -w '%{http_code}\n' \
    -F "file=@/tmp/test-$size.bin" https://example.com/upload
done
900K 200
2M 413
10M 413
50M 413
120M 413

실제 클라이언트가 본문을 그대로 보낸다면(JSON API, 오브젝트 스토리지로의 PUT) -F 대신 --data-binary @파일을 쓰세요. 테스트 업로드가 남아도 괜찮은 엔드포인트에 대고 돌리세요.

숫자가 가리키는 곳은 대략 이렇습니다.

깨지기 시작하는 크기흔한 원인
100 KB(JSON 본문)Express express.json() 기본 한도
1 MBnginx client_max_body_size 기본값, 또는 ingress-nginx 기본값
2 MB / 8 MB(413 없이 업로드만 “실패”)PHP upload_max_filesize / post_max_size 기본값
약 28.6 MB(30,000,000바이트)IIS 요청 필터링 또는 Kestrel 기본값
100 MBCloudflare Free·Pro

2단계: 어느 층이 응답했는지 찾기

상태 코드만 보지 말고 413 응답 전체를 보세요.

curl -si -F "file=@/tmp/test-2M.bin" https://example.com/upload | head -20
  • Server: nginx와 짧은 HTML 페이지 “413 Request Entity Too Large”: nginx입니다. 경로에 있는 nginx마다 오류 로그에서 client intended to send too large body: 2097364 bytes 같은 줄을 찾으세요. 이 줄이 남은 곳이 거부한 nginx입니다.
  • Cloudflare 오류 페이지, server: cloudflare, 그리고 원 서버 접근 로그에 그 요청이 아예 없음: Cloudflare 엣지입니다.
  • {"message":"request entity too large"} 같은 JSON이나 프레임워크 고유의 오류 형식: 애플리케이션입니다.
  • IIS의 HTTP Error 404.13: 요청 필터링입니다(아래 참고).

그다음 바깥 층을 건너뛰고 원 서버를 직접 시험합니다. --resolve를 쓰면 올바른 호스트명을 유지한 채 원 서버 IP로 요청을 보냅니다.

curl -s -o /dev/null -w '%{http_code}\n' \
  --resolve example.com:443:203.0.113.10 \
  -F "file=@/tmp/test-120M.bin" https://example.com/upload

공개 호스트명으로는 실패하는 크기를 원 서버가 받아 준다면, 한도는 원 서버 앞에 있습니다. DNS 조회로 호스트명이 CDN으로 가는지, HTTP 점검으로 공개 쪽의 서버·CDN 헤더를 확인할 수 있습니다.

층별로 보기

nginx: client_max_body_size

압도적으로 흔한 413 원인입니다. 기본값이 1 MB이고, nginx는 그보다 큰 본문을 뒤로 넘기기 전에 거부합니다. nginx 공식 문서도 브라우저가 이 오류를 제대로 표시하지 못할 수 있다고 적어 두었습니다. 사용자가 413이 아니라 “업로드가 멈춘다”고 말하는 이유입니다.

server {
    client_max_body_size 50m;

    location /upload {
        client_max_body_size 200m;
        proxy_pass http://app;
    }
}

http, server, location 어디에나 둘 수 있고 가장 구체적인 블록의 값이 이깁니다. http {}에서 값을 올렸는데 include된 server나 location 파일이 더 작은 값을 갖고 있는 경우가 흔한 함정입니다. 실제 적용되는 설정 전체에서 찾아보세요.

sudo nginx -T 2>/dev/null | grep -n client_max_body_size
sudo nginx -t && sudo systemctl reload nginx

client_max_body_size 0;은 검사를 끕니다. 그보다는 가장 큰 정상 업로드보다 조금 큰 실제 한도를 두는 편이 낫습니다.

nginx가 두 대라면(예를 들어 로드밸런서용 nginx와 앱 컨테이너 안의 nginx) 둘 다 바꿔야 합니다. Kubernetes의 ingress-nginx라면 Ingress별 설정은 nginx.ingress.kubernetes.io/proxy-body-size: "50m" annotation입니다.

Apache: LimitRequestBody

Apache는 LimitRequestBody(바이트 단위)로 본문을 제한하고, 서버·디렉터리·.htaccess 단위로 설정할 수 있습니다. 오랫동안 기본값은 무제한이었고, 최근 2.4 릴리스는 기본값이 1 GiB입니다. ModSecurity를 쓴다면 SecRequestBodyLimit이 별도의 한도로 역시 413을 내고, 흔히 쓰는 권장 설정 파일에는 약 12.5 MB로 잡혀 있으니 함께 확인하세요.

<Location "/upload">
    LimitRequestBody 209715200
</Location>

PHP: upload_max_filesize와 post_max_size

PHP에는 한도가 둘 있고 기본값은 2 MB와 8 MB입니다. upload_max_filesize는 파일 하나의 한도, post_max_size는 본문 전체의 한도이며 더 커야 합니다.

많은 사람이 놀라는 부분은 PHP는 413으로 답하지 않는다는 점입니다. post_max_size를 넘으면 스크립트는 그대로 실행되지만 $_POST와 $_FILES가 비어 있고, 로그에 PHP Warning: POST Content-Length of … bytes exceeds the limit of … bytes가 남습니다. upload_max_filesize를 넘으면 파일이 UPLOAD_ERR_INI_SIZE 오류 코드와 함께 들어오고, 워드프레스는 이를 “The uploaded file exceeds the upload_max_filesize directive in php.ini”로 보여 줍니다. PHP 사이트에서 진짜 413을 보고 있다면, 그건 앞단의 nginx, Apache, CDN에서 온 것입니다.

두 값은 PHP-FPM이 실제로 읽는 php.ini에서 바꾸고 PHP-FPM을 재시작하세요. CLI는 다른 파일을 읽는 경우가 많아서, php -i는 웹 서버가 쓰지 않는 값을 보여 줄 수 있습니다. 웹 서버를 통해 임시 phpinfo() 페이지를 열면 실제 값이 보입니다(확인 후 꼭 지우세요). 국내 웹호스팅을 포함한 공유 호스팅에서는 대개 .user.ini(FPM)나 .htaccess의 php_value(mod_php)로 두 값을 바꿀 수 있습니다.

애플리케이션 프레임워크

  • Express: express.json()과 express.urlencoded()의 기본 한도는 100 KB이고, 넘으면 PayloadTooLargeError: request entity too large와 함께 413을 냅니다. 필요한 라우트에 { limit: '10mb' }를 넘기세요.
  • ASP.NET Core(Kestrel): MaxRequestBodySize 기본값이 30,000,000바이트이고 413을 냅니다. 전역으로 올리거나 액션별로 [RequestSizeLimit]을 쓰세요.
  • Spring Boot: spring.servlet.multipart.max-file-size(기본 1 MB)와 max-request-size(기본 10 MB). 오류 상태 코드는 앱이 예외를 어떻게 처리하느냐에 따라 다릅니다.
  • Django: 파일 업로드가 아닌 요청 본문이 DATA_UPLOAD_MAX_MEMORY_SIZE(기본 2.5 MB)를 넘으면 413이 아니라 400으로 거부합니다. 큰 JSON POST가 애매한 Bad Request로 실패할 때 알아 두면 좋습니다.

IIS: 413 대신 404.13

IIS 요청 필터링은 본문을 기본 30,000,000바이트(약 28.6 MB)로 제한하고, 넘으면 HTTP Error 404.13 - Not Found(CONTENT_LENGTH_TOO_LARGE)로 보고합니다. 같은 문제인데 코드가 헷갈리게 나오는 것입니다. 기존 ASP.NET은 여기에 maxRequestLength(킬로바이트 단위, 기본 4096 = 4 MB)를 더합니다. 둘 다 올리세요.

<system.web>
  <httpRuntime maxRequestLength="204800" />
</system.web>
<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="209715200" />
    </requestFiltering>
  </security>
</system.webServer>

단위에 주의하세요. 앞의 것은 킬로바이트, 뒤의 것은 바이트입니다.

한도가 CDN이나 플랫폼의 것일 때

내 설정으로는 올릴 수 없는 한도도 있습니다.

Cloudflare. 프록시를 거치는 요청은 요금제별로 제한됩니다. Cloudflare 문서에는 Free·Pro 100 MB, Business 200 MB, Enterprise 기본 500 MB로 나와 있습니다. 요금제는 바뀔 수 있으니 현재 한도 페이지를 확인하세요. 한도를 넘으면 엣지가 413을 돌려주고 원 서버는 요청을 보지도 못합니다.

서버리스와 게이트웨이. AWS 문서 기준으로 API Gateway의 페이로드 한도는 10 MB, Lambda 동기 호출은 6 MB입니다. Vercel Functions의 요청 본문 한도는 약 4.5 MB입니다. Netlify, Cloud Run 등도 각자 한도가 있으니 쓰는 플랫폼의 현재 수치를 확인하세요.

이런 한도는 설정이 아니라 구조로 풉니다.

  • 오브젝트 스토리지로 바로 업로드. API가 pre-signed URL(S3, R2, GCS)을 발급하고, 브라우저가 파일을 그곳에 PUT하고, 앱은 오브젝트 키가 담긴 작은 JSON만 받습니다. 큰 파일이라면 어차피 이게 맞는 설계입니다.
  • 분할 업로드. 클라이언트에서 파일을 한도보다 작은 조각으로 나눠 보내고(S3 multipart, tus 같은 재개 가능한 업로드 프로토콜) 서버나 스토리지에서 다시 합칩니다.
  • 프록시를 거치지 않는 별도 업로드 호스트명(Cloudflare에서 DNS only). 동작은 하지만 원 서버 IP가 드러나고 그 호스트명은 CDN의 보호를 받지 못합니다. 그 엔드포인트는 그에 맞게 단단히 막아 두세요.

큰 요청이 실패하는 길이 413만은 아님

  • 시간 초과. 느린 회선에서 큰 파일을 올리면 프록시나 CDN이 기다리는 시간보다 오래 걸릴 수 있습니다. 이때는 413이 아니라 408, 504, Cloudflare 524로 나타나고, 해결책은 크기 한도가 아니라 타임아웃 조정이나 분할 업로드입니다.
  • 본문이 아니라 헤더. 작은 요청인데 쿠키가 거대하거나 URL이 아주 길면 431, 414, 또는 nginx의 “400 Request Header Or Cookie Too Large”가 납니다. 헤더 한도라서 client_max_body_size로는 해결되지 않습니다.
  • 업로드가 끝나기 전에 오는 413. 클라이언트가 크기를 미리 알리면(Content-Length나 Expect: 100-continue) 서버는 바로 거부할 수 있습니다. 브라우저가 아직 보내는 중에 서버가 연결을 닫으면 브라우저에는 연결 재설정이나 일반적인 업로드 실패로 보일 수 있습니다. curl로 보면 실제 상태가 나옵니다.

고친 다음에

  • 1단계의 크기 반복 테스트를 다시 돌리세요. 의도한 최대 크기까지는 모두 성공하고, 그 위에서는 내가 예상한 층에서 실패해야 합니다. 잊고 있던 다른 층에서 실패하면 안 됩니다.
  • 원 서버뿐 아니라 공개 호스트명으로도 다시 시험하세요.
  • 업로드 폼에 한도를 적어 두세요. “최대 50 MB” 한 줄이 어떤 서버 설정보다 문의를 많이 줄여 줍니다.

내 도메인에서 바로 확인

무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.