_closed now means the client transport has gone away. Body-wait timeouts
flip a separate _body_wait_expired flag. Both still surface as
http.disconnect to the app, but downstream code can now distinguish 'the
socket is dead' from 'the body never finished framing in time' without
guessing which path set the flag.
- ASGI keepalive gate now keys on receiver._complete only. _closed is
overloaded across transport disconnect and receive timeout; treating
either as 'message complete' would re-enable the smuggling vector
the previous PR was meant to close.
- Parser.finish_body's 64 KiB byte cap now applies only when an explicit
deadline is given. Default invocations (notably __next__, used by
base_async / sync workers) regain the prior unbounded drain so a
partial drain does not silently desync the next request.
- WSGI fast parser now applies the same per-header policy as the Python
parser (Expect, secure_scheme_headers, forwarded_allow_ips trust gate,
forwarder_headers / header_map). Shared helpers extracted on Message.
- ASGI keepalive no longer resets the parser when the previous request
body was not fully framed; the connection closes instead, preventing
request smuggling on pipelined connections.
- BodyReceiver._wait_for_data timeout flips _closed and yields
http.disconnect rather than synthesizing more_body=False. Timeout
honors cfg.timeout.
- ASGI chunked encoding now skips HEAD, 204, and 304 (matches
Response.is_chunked in the WSGI path) via a small helper.
- _setup_callback_parser passes proxy_protocol to PythonProtocol; auto
falls back to the Python parser when proxy_protocol != off (the C
parser does not implement PROXY framing). _effective_peername swaps
the transport peer with the PROXY-supplied client address.
- Parser.finish_body accepts a deadline and a 64KiB byte cap; gthread
passes a deadline and abandons keepalive on incomplete drain so a
stalled client cannot tie up a worker thread.
- Replace datetime.now() with time.monotonic() for request timing
- Add access_log_enabled property to skip log work when disabled
- Rewrite BodyReceiver with Future-based waiting (no create_task)
- Remove StreamReader for HTTP/1.1, use direct bytearray buffering
- Add BufferReader wrapper for FastAsyncRequest compatibility
- Use pre-cached chunk prefixes in _send_body()
- Convert async methods to sync where no await needed
- Batch response writes (headers + body in single write)
Performance: 4,200 -> 69,500 req/s
Wire HttpParser to ASGI hot path, replacing AsyncRequest.parse() with
direct buffer-based parsing. Add FastAsyncRequest wrapper for body
reading. Replace per-request Queue/Task with BodyReceiver for on-demand
body reading. Keep headers as bytes end-to-end to avoid conversion
overhead. Add backpressure control and keepalive timer. Cache response
status lines and Date header.
Benchmark shows 3x improvement: ~875K req/s for simple GET (was ~340K).
Closes#3484
When a client disconnects during an ASGI request, the worker now:
1. Sends http.disconnect message to the app's receive queue
2. Allows a configurable grace period for cleanup (default: 3 seconds)
3. Only cancels the task after the grace period expires
This follows the ASGI HTTP Connection Scope spec which defines
http.disconnect as the message apps should receive when clients
disconnect: https://asgi.readthedocs.io/en/latest/specs/www.html#disconnect-receive-event
The grace period prevents CancelledError from propagating to async
database operations, allowing SQLAlchemy and other async DB libraries
to properly reset their connection pools.
New config option: --asgi-disconnect-grace-period (default: 3 seconds)