#1726·Nginx

empty-body HTTP/2 upstream responses are never marked keepalive-safe

Author: bungleCreated Sep 4, 2026Updated Sep 15, 2026
Labelsbug

Bug Overview

Component: ngx_http_proxy_v2_module.c · Affects: 1.29.4 onward, confirmed still present in 1.31.5

Root cause: ngx_http_proxy_v2_filter_init() short-circuits when headers already carry END_STREAM (i.e. an empty body, e.g. Content-Length: 0):

c
if (ctx->end_stream) {
    ...
    u->length = 0;
    u->pipe->length = 0;
    ctx->done = 1;
}

This means the generic pipe/read loop never calls ngx_http_proxy_v2_process_frames() — the only other place in the module that ever sets u->keepalive = 1. Every subsequent request opens a fresh connection.

Fix, verified to fully resolve it alone, on stock nginx (TIME_WAIT stays flat under load, "keepalive saving/reusing connection" behaves identically to HTTP/1.1):

c
if (ctx->end_stream) {
    ...
    u->length = 0;
    u->pipe->length = 0;
    ctx->done = 1;
      
    if (ctx->in == NULL
        && ctx->output_closed
        && !ctx->output_blocked
        && !ctx->goaway
        && ctx->state == ngx_http_proxy_v2_st_start)
    {   
        u->keepalive = 1;
    }   
}       

Expected Behavior

an HTTP/2 upstream connection with a cleanly-completed empty-body response should be pooled and reused on the next request, exactly like HTTP/1.1 already does — instead it's silently closed every time.

Steps to Reproduce the Bug

  1. Configure an upstream block with keepalive N; and a backend that returns a response with an empty body (e.g. Content-Length: 0, status 200).
  2. Proxy to it with proxy_pass + proxy_http_version 2;.
  3. Send two sequential requests to the proxying location.
  4. Check debug_error_log: expect "get keepalive peer: using connection" on the 2nd request — instead you get "free keepalive peer" with no "saving connection", and a brand-new connection every time.

NGINX Configuration

nginx
worker_processes 1;
error_log logs/error.log debug;
  
events {
    worker_connections 1024;
}
  
http {
    upstream backend {
        server 127.0.0.1:9999;
        keepalive 4;
    }
          
    server {
        listen 9999;
        http2 on;
    
        location / { 
            return 200 "";   # empty body, Content-Length: 0
        }
    }   
              
    server {
        listen 9998;
          
        location / {
            proxy_pass http://backend;
            proxy_http_version 2;
        }
    }       
}

NGINX version and build configuration options

The output of nginx -V:

nginx version: nginx/1.31.5
  built by gcc 14.2.1 20250405 (GCC)
  configure arguments: --prefix=/tmp/nginx-vcheck/install --with-http_v2_module --with-debug

Environment where NGINX is being built and/or deployed

  • Target OS: Void Linux

Architecture where NGINX is being built and/or deployed

The output of uname -a:

Linux kong 6.18.49_1 #1 SMP PREEMPT_DYNAMIC Wed Sep  2 14:10:52 UTC 2026 x86_64 GNU/Linux

NGINX Debug Log

No response

Additional Context

No response