diff --git a/docs/content/guides/http2.md b/docs/content/guides/http2.md index 902d0d0c..34f3d230 100644 --- a/docs/content/guides/http2.md +++ b/docs/content/guides/http2.md @@ -225,7 +225,7 @@ server { !!! note For nginx to forward 103 Early Hints from upstream, you need nginx 1.29+ - and the `early_hints` directive. + and the [`early_hints`](https://nginx.org/en/docs/http/ngx_http_core_module.html#early_hints) directive. ### Direct TLS Termination @@ -312,10 +312,10 @@ http2_max_concurrent_streams = 200 # Increase from default 100 ```bash # Check HTTP/2 support -curl -v --http2 https://localhost:8443/ +curl -v --http2 https://localhost:443/ # Force HTTP/2 -curl --http2-prior-knowledge https://localhost:8443/ +curl --http2-prior-knowledge https://localhost:443/ ``` ### Using Python diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py index f26555d6..9db7d443 100644 --- a/gunicorn/http/wsgi.py +++ b/gunicorn/http/wsgi.py @@ -107,7 +107,7 @@ def proxy_environ(req): } -def _make_early_hints_callback(req, sock): +def _make_early_hints_callback(req, sock, resp): """Create a wsgi.early_hints callback for sending 103 Early Hints. This allows WSGI applications to send 103 Early Hints responses @@ -116,6 +116,7 @@ def _make_early_hints_callback(req, sock): Args: req: The request object sock: The socket to write to + resp: The Response object to check if headers have been sent Returns: A callback function that accepts a list of (name, value) header tuples @@ -125,6 +126,7 @@ def _make_early_hints_callback(req, sock): - Early hints are only sent for HTTP/1.1 or later clients - HTTP/1.0 clients will silently ignore the callback - Multiple calls are allowed (sending multiple 103 responses) + - Calls after response has started are silently ignored """ def send_early_hints(headers): """Send 103 Early Hints response. @@ -133,6 +135,10 @@ def _make_early_hints_callback(req, sock): headers: List of (name, value) header tuples, typically Link headers Example: [('Link', '; rel=preload; as=style')] """ + # Don't send after response has started - would break framing + if resp.headers_sent: + return + # Don't send to HTTP/1.0 clients - they don't support 1xx responses if req.version < (1, 1): return @@ -242,7 +248,7 @@ def create(req, sock, client, server, cfg): environ.update(proxy_environ(req)) # Add wsgi.early_hints callback for sending 103 Early Hints - environ['wsgi.early_hints'] = _make_early_hints_callback(req, sock) + environ['wsgi.early_hints'] = _make_early_hints_callback(req, sock, resp) return resp, environ diff --git a/gunicorn/http2/request.py b/gunicorn/http2/request.py index b1c2ac40..50683c48 100644 --- a/gunicorn/http2/request.py +++ b/gunicorn/http2/request.py @@ -133,8 +133,10 @@ class HTTP2Request: # Convert to uppercase for WSGI compatibility self.headers.append((name.upper(), value)) - # Add Host header if not present (from :authority) - if authority and not any(h[0] == 'HOST' for h in self.headers): + # Set Host header from :authority (RFC 9113 section 8.3.1) + # :authority MUST take precedence over Host header + if authority: + self.headers = [(n, v) for n, v in self.headers if n != 'HOST'] self.headers.append(('HOST', authority)) # Trailers (if any)