Nginx Proxy Behavior: Why Content-Length Header is Dropped for Chunked Transfer Encoding


2 views

When working with HTTP responses, having both Transfer-Encoding: chunked and Content-Length headers is technically invalid according to RFC 7230 Section 3.3.3:

// RFC 7230 states:
"A sender MUST NOT send a Content-Length header field in any message
that contains a Transfer-Encoding header field."

Nginx (especially newer versions) strictly follows HTTP specifications. When it detects chunked encoding:

  1. Removes Content-Length automatically
  2. Processes the chunks properly
  3. May buffer chunks when proxy_buffering is on

If you absolutely need both headers (for legacy client compatibility), try:

location /your-endpoint {
    proxy_pass http://backend;
    proxy_http_version 1.0;  # Downgrade to HTTP 1.0
    proxy_set_header Connection "";
    chunked_transfer_encoding off;  # Disable chunked encoding
}

To inspect Nginx's header processing:

# In nginx.conf
log_format headers '$remote_addr - $remote_user [$time_local] '
                   '"$request" $status $body_bytes_sent '
                   '"$http_referer" "$http_user_agent" '
                   'ReqHeaders: "$req_headers" RespHeaders: "$resp_headers"';

server {
    set $resp_headers "";
    add_header X-Debug-Resp-Headers $resp_headers;

    location / {
        proxy_pass http://backend;
        header_filter_by_lua_block {
            ngx.var.resp_headers = table.concat(ngx.resp.get_headers(), ", ")
        }
    }
}

Removing Content-Length with chunked encoding is actually beneficial:

  • Avoids client confusion about payload size
  • Allows streaming of dynamic content
  • Prevents buffer overrun issues

When working with Nginx as a reverse proxy (version 1.2.3 in this case), I encountered an interesting behavior where the proxy automatically removes the Content-Length header when the upstream server responds with chunked transfer encoding. Here's the exact proxy configuration:

proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8880;
proxy_buffering off;
proxy_read_timeout 300s;
gzip off;

The backend script returns both headers:

HTTP/1.0 307 Temporary Redirect
Content-length: 251
Pragma: no-cache
Location: /...
Cache-control: no-cache
Transfer-encoding: chunked

After passing through Nginx, the response becomes:

HTTP/1.1 302 Found
Server: nginx/1.2.3
Content-Type: application/json; charset=utf-8
Content-Length: 58
Connection: keep-alive
Location: /...

This behavior stems from HTTP/1.1 specifications (RFC 2616 section 4.4) which states that when Transfer-Encoding is present (especially chunked), the Content-Length header must be ignored. Nginx implements this strictly for HTTP/1.1 compliance.

In earlier versions (pre-1.1), Nginx was more lenient with header combinations, which explains why this "worked" before but doesn't now.

Option 1: Force HTTP/1.0 protocol

proxy_http_version 1.0;

Option 2: Use headers-more module to manually preserve the header

more_set_headers "Content-Length: 251";

Option 3: Modify your application logic to:

if ($http_transfer_encoding ~* chunked) {
    # handle chunked responses differently
    ...
}

Modern HTTP clients should handle chunked encoding properly without requiring Content-Length. If you're dealing with legacy clients, consider:

  • Implementing proper chunked encoding handling on client side
  • Adding client-side timeout logic
  • Using websockets or other protocols if real-time streaming is needed

Removing proxy_buffering (as in your config) is generally good for streaming scenarios but may impact performance for large payloads. Benchmark both approaches:

proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;