gunicorn/gunicorn/http2/request.py
2026-01-27 09:59:35 +01:00

229 lines
6.1 KiB
Python

# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
HTTP/2 request wrapper.
Provides a Request-compatible interface for HTTP/2 streams.
"""
from io import BytesIO
from gunicorn.util import split_request_uri
class HTTP2Body:
"""Body wrapper for HTTP/2 request data.
Provides a file-like interface to the request body,
compatible with gunicorn's Body class expectations.
"""
def __init__(self, data):
"""Initialize with body data.
Args:
data: bytes containing the request body
"""
self._data = BytesIO(data)
self._len = len(data)
def read(self, size=None):
"""Read data from the body.
Args:
size: Number of bytes to read, or None for all remaining
Returns:
bytes: The requested data
"""
if size is None:
return self._data.read()
return self._data.read(size)
def readline(self, size=None):
"""Read a line from the body.
Args:
size: Maximum bytes to read
Returns:
bytes: A line of data
"""
if size is None:
return self._data.readline()
return self._data.readline(size)
def readlines(self, hint=None):
"""Read all lines from the body.
Args:
hint: Approximate byte count hint
Returns:
list: List of lines
"""
return self._data.readlines(hint)
def __iter__(self):
"""Iterate over lines in the body."""
return iter(self._data)
def __len__(self):
"""Return the content length."""
return self._len
def close(self):
"""Close the body stream."""
self._data.close()
class HTTP2Request:
"""HTTP/2 request wrapper compatible with gunicorn Request interface.
Wraps an HTTP2Stream to provide the same interface as the HTTP/1.x
Request class, allowing workers to handle HTTP/2 requests using
existing code paths.
"""
def __init__(self, stream, cfg, peer_addr):
"""Initialize from an HTTP/2 stream.
Args:
stream: HTTP2Stream instance with received headers/body
cfg: Gunicorn configuration object
peer_addr: Client address tuple (host, port)
"""
self.stream = stream
self.cfg = cfg
self.peer_addr = peer_addr
self.remote_addr = peer_addr
# HTTP/2 version tuple
self.version = (2, 0)
# Parse pseudo-headers
pseudo = stream.get_pseudo_headers()
self.method = pseudo.get(':method', 'GET')
self.scheme = pseudo.get(':scheme', 'https')
authority = pseudo.get(':authority', '')
path = pseudo.get(':path', '/')
# Parse the path into components
self.uri = path
try:
parts = split_request_uri(path)
self.path = parts.path or ""
self.query = parts.query or ""
self.fragment = parts.fragment or ""
except ValueError:
self.path = path
self.query = ""
self.fragment = ""
# Store authority for Host header equivalent
self._authority = authority
# Convert HTTP/2 headers to HTTP/1.1 style
# HTTP/2 headers are lowercase, convert to uppercase for WSGI
self.headers = []
for name, value in stream.get_regular_headers():
# 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):
self.headers.append(('HOST', authority))
# Trailers (if any)
self.trailers = []
if stream.trailers:
self.trailers = [
(name.upper(), value)
for name, value in stream.trailers
]
# Body - HTTP/2 streams have complete body data
body_data = stream.get_request_body()
self.body = HTTP2Body(body_data)
# Connection state
self.must_close = False
self._expected_100_continue = False
# Request numbering (for logging)
self.req_number = stream.stream_id
# HTTP/2 does not use proxy protocol through the data stream
self.proxy_protocol_info = None
def force_close(self):
"""Force the connection to close after this request."""
self.must_close = True
def should_close(self):
"""Check if connection should close after this request.
HTTP/2 connections are persistent by design, but we may still
need to close if explicitly requested.
Returns:
bool: True if connection should close
"""
if self.must_close:
return True
# HTTP/2 connections are persistent, don't close by default
return False
def get_header(self, name):
"""Get a header value by name.
Args:
name: Header name (case-insensitive)
Returns:
str: Header value, or None if not found
"""
name = name.upper()
for h_name, h_value in self.headers:
if h_name == name:
return h_value
return None
@property
def content_length(self):
"""Get the Content-Length header value.
Returns:
int: Content length, or None if not set
"""
cl = self.get_header('CONTENT-LENGTH')
if cl is not None:
try:
return int(cl)
except ValueError:
pass
return None
@property
def content_type(self):
"""Get the Content-Type header value.
Returns:
str: Content type, or None if not set
"""
return self.get_header('CONTENT-TYPE')
def __repr__(self):
return (
f"<HTTP2Request "
f"method={self.method} "
f"path={self.path} "
f"stream_id={self.stream.stream_id}>"
)
__all__ = ['HTTP2Request', 'HTTP2Body']