A production-quality pure-Python WSGI server with robust HTTP protocol support and comprehensive configuration options
—
Low-level HTTP request parsing, connection management, and protocol handling for custom server implementations and advanced use cases.
Handles individual HTTP client connections with full request/response lifecycle management.
class HTTPChannel(wasyncore.dispatcher):
"""
Manages individual HTTP client connections with full request/response lifecycle.
Handles request parsing, WSGI application execution, response transmission,
connection keep-alive, and proper resource cleanup for a single client.
"""
# Class attributes
task_class = WSGITask # Task class for WSGI execution
error_task_class = ErrorTask # Task class for error responses
parser_class = HTTPRequestParser # Parser class for requests
def __init__(self, server, sock, addr, adj, map=None):
"""
Initialize HTTP channel for client connection.
Parameters:
- server: Parent server instance
- sock: Client socket object
- addr: Client address tuple (host, port)
- adj: Adjustments configuration object
- map: asyncore socket map (optional)
"""
def check_client_disconnected(self):
"""
Check if client has disconnected.
Returns:
bool: True if client is disconnected
Notes:
- Inserted into WSGI environ for applications to check
- Allows long-running operations to detect disconnection
"""
def received(self, data):
"""
Process received data from client.
Parameters:
- data (bytes): Raw HTTP data from client
Notes:
- Handles incremental request parsing
- Manages request buffering and validation
- Triggers request processing when complete
"""
def handle_request(self, req):
"""
Handle complete HTTP request.
Parameters:
- req: Parsed request object with WSGI environ
Notes:
- Creates appropriate task (WSGI or Error)
- Manages connection keep-alive
- Handles 100 Continue responses
"""
def build_response_header(self, status, headers):
"""
Build HTTP response header from WSGI components.
Parameters:
- status (str): HTTP status line (e.g., "200 OK")
- headers (list): List of (name, value) header tuples
Returns:
bytes: Complete HTTP response header
"""
def write_soon(self, data):
"""
Queue data for asynchronous transmission to client.
Parameters:
- data (bytes): Response data to send
Notes:
- Thread-safe buffering system
- Automatic flow control and backpressure
- Handles large response streaming
"""
def writable(self):
"""
Check if channel has data ready to send.
Returns:
bool: True if data waiting to be sent
"""
def handle_write(self):
"""Send buffered data to client socket."""
def handle_read(self):
"""Read and process data from client socket."""
def handle_close(self):
"""Clean up resources when connection closes."""
class ClientDisconnected(Exception):
"""
Raised when client disconnects unexpectedly during request processing.
This exception is raised when:
- Client closes connection during request processing
- Socket errors indicate disconnection
- Write operations fail due to broken connection
"""Comprehensive HTTP request parser with support for HTTP/1.0 and HTTP/1.1 protocols.
class HTTPRequestParser:
"""
Parses HTTP requests into WSGI environ dictionaries.
Supports HTTP/1.0 and HTTP/1.1 with proper handling of:
- Request line parsing
- Header field parsing and validation
- Content-Length and Transfer-Encoding
- Connection management headers
"""
def __init__(self, adj):
"""
Initialize request parser.
Parameters:
- adj: Adjustments configuration for limits and behavior
"""
def received(self, data, pos=0):
"""
Process received request data.
Parameters:
- data (bytes): Raw HTTP request data
- pos (int): Starting position in data
Returns:
int: Number of bytes consumed
"""
def parse_headers(self, header_plus):
"""Parse HTTP headers into environ dict."""
def get_body_stream(self):
"""Get request body as file-like object."""
class ParsingError(Exception):
"""
Raised when HTTP request parsing fails.
Indicates malformed requests, protocol violations,
or requests exceeding configured limits.
"""
class TransferEncodingNotImplemented(Exception):
"""
Raised for unsupported Transfer-Encoding values.
Currently only 'chunked' and 'identity' are supported.
"""Low-level functions for parsing HTTP protocol elements.
def unquote_bytes_to_wsgi(bytestring):
"""
URL decode bytes to WSGI-compatible string.
Parameters:
- bytestring (bytes): URL-encoded bytes
Returns:
str: Decoded string safe for WSGI environ
"""
def split_uri(uri):
"""
Parse URI into components.
Parameters:
- uri (str): Complete URI string
Returns:
tuple: (scheme, netloc, path, query, fragment)
"""
def get_header_lines(header):
"""
Split HTTP header block into individual header lines.
Parameters:
- header (str): Complete header block
Returns:
list: Individual header lines
"""
def crack_first_line(line):
"""
Parse HTTP request line.
Parameters:
- line (str): HTTP request line (e.g., "GET /path HTTP/1.1")
Returns:
tuple: (method, uri, version)
Raises:
ValueError: For malformed request lines
"""Common patterns for working with HTTP processing components.
from waitress.channel import HTTPChannel
from waitress.adjustments import Adjustments
class CustomHTTPChannel(HTTPChannel):
"""Custom HTTP channel with additional logging."""
def received(self, data):
# Log incoming data
print(f"Received {len(data)} bytes from {self.addr}")
return super().received(data)
def handle_request(self, req):
# Custom request processing
print(f"Processing {req['REQUEST_METHOD']} {req['PATH_INFO']}")
return super().handle_request(req)
# Use custom channel in server
adj = Adjustments()
# Server configuration would use CustomHTTPChannelfrom waitress.parser import HTTPRequestParser
from waitress.adjustments import Adjustments
adj = Adjustments(max_request_header_size=65536)
parser = HTTPRequestParser(adj)
# Simulate receiving HTTP request data
request_data = b"""GET /hello HTTP/1.1\r
Host: example.com\r
User-Agent: Test/1.0\r
\r
"""
try:
bytes_consumed = parser.received(request_data)
if parser.completed:
environ = parser.environ
print(f"Method: {environ['REQUEST_METHOD']}")
print(f"Path: {environ['PATH_INFO']}")
print(f"Headers parsed successfully")
except ParsingError as e:
print(f"Parsing failed: {e}")from waitress.parser import split_uri, crack_first_line, get_header_lines
# Parse request line
method, uri, version = crack_first_line("GET /api/users?page=1 HTTP/1.1")
print(f"Method: {method}, URI: {uri}, Version: {version}")
# Parse URI components
scheme, netloc, path, query, fragment = split_uri(uri)
print(f"Path: {path}, Query: {query}")
# Process headers
header_block = """Content-Type: application/json\r
Content-Length: 123\r
Authorization: Bearer token123"""
header_lines = get_header_lines(header_block)
for line in header_lines:
if ':' in line:
name, value = line.split(':', 1)
print(f"Header: {name.strip()} = {value.strip()}")Waitress HTTP processing supports comprehensive HTTP protocol features.
# Supported HTTP versions
HTTP_VERSIONS = ["HTTP/1.0", "HTTP/1.1"]
# Supported request methods
HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH", "TRACE"]
# Connection handling
KEEPALIVE_SUPPORT = True # HTTP/1.1 persistent connections
PIPELINING_SUPPORT = False # HTTP pipelining not supported
# Transfer encodings
SUPPORTED_ENCODINGS = ["identity", "chunked"]
# Content encodings (handled by application)
CONTENT_ENCODING_PASSTHROUGH = TrueHTTP processing can raise various exceptions for different error conditions.
# Common parsing errors:
# - Request line malformed: ValueError from crack_first_line()
# - Headers too large: ParsingError with size limit exceeded
# - Invalid transfer encoding: TransferEncodingNotImplemented
# - Connection errors: ClientDisconnected during processing
# - Protocol violations: ParsingError with description
# Example error handling
try:
parser.received(request_data)
except ParsingError as e:
# Send 400 Bad Request
response = b"HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n"
except TransferEncodingNotImplemented as e:
# Send 501 Not Implemented
response = b"HTTP/1.1 501 Not Implemented\r\nConnection: close\r\n\r\n"
except ClientDisconnected:
# Clean up connection
passHTTP processing creates proper WSGI environ dictionaries.
# Required WSGI environ keys populated:
REQUEST_METHOD = "GET" # HTTP method
SCRIPT_NAME = "" # Application script name
PATH_INFO = "/path" # Request path
QUERY_STRING = "param=value" # Query parameters
CONTENT_TYPE = "" # Content-Type header
CONTENT_LENGTH = "" # Content-Length header
SERVER_NAME = "localhost" # Server hostname
SERVER_PORT = "8080" # Server port
SERVER_PROTOCOL = "HTTP/1.1" # HTTP version
wsgi.version = (1, 0) # WSGI version
wsgi.url_scheme = "http" # URL scheme
wsgi.input = file_object # Request body stream
wsgi.errors = sys.stderr # Error stream
wsgi.multithread = True # Threading model
wsgi.multiprocess = False # Process model
wsgi.run_once = False # Execution model
# HTTP headers converted to HTTP_* keys:
HTTP_HOST = "example.com" # Host header
HTTP_USER_AGENT = "Browser" # User-Agent header
HTTP_ACCEPT = "text/html" # Accept header
# etc.Install with Tessl CLI
npx tessl i tessl/pypi-waitress