Unexpeted code 200 instead of 413

1. The problem I’m having:

I want caddy to always read the complete POST body, I doubt it does.
I tried to use a simple POST endpoint and sent arbitrary amounts of data to that endpoint. No matter how big the request was, caddy returned a 204 within several milliseconds:

{
    ### Global options
    #   no admin interface
    admin off

    #   log to stdout for systemd or Docker
    log {
        output stdout
        format console
        level debug
    }

    debug
}

# -------------------------------------------------
# HTTP listener
# -------------------------------------------------
http:// {
    request_body  {
        max_size 3KiB
    }

    handle /upload {
        respond "" 204
    }

    handle {
        respond "Not found" 404
    }
}

Now I try to pass the request through a reverse proxy. I’ve put the request_body directive to many places but I cannot get caddy to respond with 413 on too large input. I use curl for my tests:

$ time curl -ki -H 'Content-type: application/octet-stream' --data-binary @verylargebin --noproxy \* http://10.100.200.32:10080/upload
HTTP/1.1 100 Continue

HTTP/1.1 200 OK
Connection: close
Content-Length: 0
Date: Sun, 08 Feb 2026 14:56:05 GMT
Server: Caddy
Via: 1.1 Caddy


real	0m0,044s
user	0m0,013s
sys	    0m0,031s

verylargebin is a 15MB file, using 1GB results in an out of memory error.
My actual goal is to measure the time required for valid HTTP POST requests. Any idea where I am wrong? I would happily skip the reverse proxy if there is an easier way.

2. Error messages and/or full log output:

2026/02/08 14:50:35.997	DEBUG	http.handlers.reverse_proxy	selected upstream	{"dial": "127.0.0.1:60080", "total_upstreams": 1}
2026/02/08 14:50:35.997	DEBUG	http.handlers.reverse_proxy	upstream roundtrip	{"upstream": "127.0.0.1:60080", "duration": 0.000463496, "request": {"remote_ip": "10.100.200.32", "remote_port": "50320", "client_ip": "10.100.200.32", "proto": "HTTP/1.1", "method": "POST", "host": "10.100.200.32:10080", "uri": "/upload", "headers": {"Content-Type": ["application/octet-stream"], "Content-Length": ["15728640"], "Expect": ["100-continue"], "X-Forwarded-For": ["10.100.200.32"], "X-Forwarded-Host": ["10.100.200.32:10080"], "Via": ["1.1 Caddy"], "User-Agent": ["curl/8.5.0"], "Accept": ["*/*"], "X-Forwarded-Proto": ["http"]}}, "headers": {"Server": ["Caddy"], "Date": ["Sun, 08 Feb 2026 14:50:35 GMT"], "Content-Length": ["0"]}, "status": 200}

3. Caddy version:

$ caddy --version
v2.10.2 h1:g/gTYjGMD0dec+UgMw8SnfmJ3I9+M2TdvoRL/Ovu6U8=

4. How I installed and ran Caddy:

docker run --rm \
    -p 10080:80\
    --user "$(id -u):$(id -g)" \
    -v $(pwd)/conf:/config \
    -v $(pwd)/conf:/etc/caddy \
    -v $(pwd)/data:/data \
    -v $(pwd)/certs:/etc/caddy/certs:ro \
    -v $(pwd)/large.bin:/srv/static/large.bin:ro \
    -e 'CORS_ORIGINS=https://localhost,https://[::1]' \
    -e 'MAX_CONNS=20' \
    -e 'MAX_UPLOAD_SIZE=2097152' \
    caddy:2

a. System environment:

Docker version 27.2.1

b. Command:

caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

c. Service/unit/compose file:

d. My complete Caddy config:

{
    ### Global options
    #   no admin interface
    admin off

    #   log to stdout for systemd or Docker
    log {
        output stdout
        format console
        level debug
    }

    debug
}

#####   upload size limiter <= NOT GLOBALLY
####@upload-api path /upload

# -------------------------------------------------
# internal reverse proxy endpoint
# -------------------------------------------------
http://127.0.0.1:60080 {
    request_body {
        max_size 3KiB
    }
    handle /upload {
        header +X-Reverse-Proxy-Handler yes
        respond "Read the file" 200
        #import upload
    }

    handle {
        respond "Not found" 404
    }
}

# -------------------------------------------------
# HTTP listener
# -------------------------------------------------
http:// {
    request_body  {
        max_size 3KiB
    }

    handle /upload {
        #request_body {
        #    max_size 3KiB
        #}
        reverse_proxy http://127.0.0.1:60080
    }

    handle {
        respond "Not found" 404
    }
}

5. Links to relevant resources:

Now I once encountered 413 with this configuration:

    ### Global options
    #   no admin interface
    admin off

    #   log to stdout for systemd or Docker
    log {
        output stdout
        format console
        level debug
    }

    debug
}

# -------------------------------------------------
# internal reverse proxy endpoint
# -------------------------------------------------
http://127.0.0.1:60080 {
    handle /upload {
        header +X-Reverse-Proxy-Handler yes
        respond "" 204
    }

    handle {
        respond "Not found" 404
    }
}

# -------------------------------------------------
# HTTP listener
# -------------------------------------------------
http:// {
    request_body  {
        max_size 3KiB
    }

    handle /upload {
        reverse_proxy http://127.0.0.1:60080
    }

    handle {
        respond "Not found" 404
    }
}

These were my log lines:

2026/02/08 15:27:58.762	DEBUG	http.handlers.reverse_proxy	selected upstream	{"dial": "127.0.0.1:60080", "total_upstreams": 1}
2026/02/08 15:27:58.762	DEBUG	http.handlers.reverse_proxy	upstream roundtrip	{"upstream": "127.0.0.1:60080", "duration": 0.000581973, "request": {"remote_ip": "10.100.200.32", "remote_port": "48884", "client_ip": "10.100.200.32", "proto": "HTTP/1.1", "method": "POST", "host": "10.100.200.32:10080", "uri": "/upload", "headers": {"Content-Length": ["524288000"], "X-Forwarded-For": ["10.100.200.32"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Host": ["10.100.200.32:10080"], "Via": ["1.1 Caddy"], "Content-Type": ["application/octet-stream"], "Expect": ["100-continue"], "User-Agent": ["curl/8.5.0"], "Accept": ["*/*"]}}, "error": "readfrom tcp 127.0.0.1:55172->127.0.0.1:60080: {id=fmftd5rvb} requestbody.errorWrapper.Read (requestbody.go:117): HTTP 413: http: request body too large"}
2026/02/08 15:27:58.763	DEBUG	http.log.error	http: request body too large	{"request": {"remote_ip": "10.100.200.32", "remote_port": "48884", "client_ip": "10.100.200.32", "proto": "HTTP/1.1", "method": "POST", "host": "10.100.200.32:10080", "uri": "/upload", "headers": {"User-Agent": ["curl/8.5.0"], "Accept": ["*/*"], "Content-Type": ["application/octet-stream"], "Content-Length": ["524288000"], "Expect": ["100-continue"]}}, "duration": 0.000724988, "status": 413, "err_id": "fmftd5rvb", "err_trace": "requestbody.errorWrapper.Read (requestbody.go:117)"}

Executing the same curl command again I get 200 responses :thinking:

2026/02/08 15:28:03.198	DEBUG	http.handlers.reverse_proxy	selected upstream	{"dial": "127.0.0.1:60080", "total_upstreams": 1}
2026/02/08 15:28:03.198	DEBUG	http.handlers.reverse_proxy	upstream roundtrip	{"upstream": "127.0.0.1:60080", "duration": 0.000291989, "request": {"remote_ip": "10.100.200.32", "remote_port": "56002", "client_ip": "10.100.200.32", "proto": "HTTP/1.1", "method": "POST", "host": "10.100.200.32:10080", "uri": "/upload", "headers": {"X-Forwarded-Host": ["10.100.200.32:10080"], "User-Agent": ["curl/8.5.0"], "Accept": ["*/*"], "Content-Length": ["524288000"], "X-Forwarded-For": ["10.100.200.32"], "Via": ["1.1 Caddy"], "Content-Type": ["application/octet-stream"], "Expect": ["100-continue"], "X-Forwarded-Proto": ["http"]}}, "headers": {"Server": ["Caddy"], "Date": ["Sun, 08 Feb 2026 15:28:03 GMT"], "Content-Length": ["0"]}, "status": 200}
2026/02/08 15:28:33.206	DEBUG	http.handlers.reverse_proxy	selected upstream	{"dial": "127.0.0.1:60080", "total_upstreams": 1}
2026/02/08 15:28:33.207	DEBUG	http.handlers.reverse_proxy	upstream roundtrip	{"upstream": "127.0.0.1:60080", "duration": 0.000264885, "request": {"remote_ip": "10.100.200.32", "remote_port": "57820", "client_ip": "10.100.200.32", "proto": "HTTP/1.1", "method": "POST", "host": "10.100.200.32:10080", "uri": "/upload", "headers": {"Content-Length": ["524288000"], "Expect": ["100-continue"], "X-Forwarded-Host": ["10.100.200.32:10080"], "Via": ["1.1 Caddy"], "User-Agent": ["curl/8.5.0"], "Content-Type": ["application/octet-stream"], "X-Forwarded-For": ["10.100.200.32"], "X-Forwarded-Proto": ["http"], "Accept": ["*/*"]}}, "headers": {"Server": ["Caddy"], "Date": ["Sun, 08 Feb 2026 15:28:33 GMT"], "Content-Length": ["0"]}, "status": 200}

It’s not even a 204 as configured…