# Protocol Readiness Progress
This document tracks protocol hardening progress and the remaining work needed to make the advanced protocol stack production-ready.
## Current Progress
### SSE
- Streaming SSE parsing is available.
- `Content-Type` validation for `text/event-stream` is in place.
- `retry` field parsing is supported.
- `Last-Event-ID` request helper is available via `RequestBuilder::last_event_id(...)`.
- End-to-end HTTP/2 coverage exists for `Last-Event-ID` request flow.
### HTTP/1.1
- Proxy CONNECT tunnel handshake correctly emits a single blank line (`\r\n`) terminating the request, not two (was accidentally sending `\r\n\r\n\r\n`).
- HTTP CONNECT tunnel requests now emit a valid `CONNECT host:port HTTP/1.1`
request line in both the HTTP/1 and HTTP/2 proxy transport paths.
- Proxy CONNECT response parser accepts both `HTTP/1.1 200` and `HTTP/2 200` status lines.
- SOCKS5 handshake read/write operations are all wrapped with `with_timeout_io`, preventing hangs on unresponsive proxies.
- End-to-end HTTPS proxy tunnel coverage now exists for:
- HTTP CONNECT without auth
- HTTP CONNECT with auth
- SOCKS5 without auth
- SOCKS5 with auth
- End-to-end HTTPS proxy tunnel coverage now also exists for HTTP/2 over:
- HTTP CONNECT
- SOCKS5
### HTTP/2
- Experimental implementation has been pushed much closer to production behavior.
- Peer `SETTINGS_MAX_HEADER_LIST_SIZE` is enforced before sending requests.
- Peer `SETTINGS_HEADER_TABLE_SIZE` now drives a stateful HPACK encoder that:
- emits dynamic table size updates when the peer limit changes
- reuses dynamic-table entries when they fit inside the peer limit
- falls back to non-reused literals when entries exceed the peer limit
- Connection reuse coverage is in place.
- GOAWAY handling and reconnect coverage are in place.
- Keepalive configuration is now exposed via:
- `ClientBuilder::h2_keepalive(...)`
- `ClientBuilder::disable_h2_keepalive()`
- `RequestBuilder::h2_keepalive(...)`
- `RequestBuilder::disable_h2_keepalive()`
- Connection pool keys no longer include H2 keepalive configuration, avoiding
unnecessary pool fragmentation. Keepalive remains a connection-creation-time
behavior rather than part of pool identity.
- `SETTINGS_ENABLE_PUSH` from the server is silently ignored (treated as a no-op) rather than returning a protocol error, matching RFC 9113 which deprecates the setting for clients.
- `FrameType::Unknown` serialization correctly panics with `unreachable!()` instead of emitting a zero byte, preventing silent wire corruption.
- Background keepalive tasks use the correct `std::thread::spawn` + `async_io::block_on` pattern rather than the non-existent `async_io::spawn` API.
- `should_retry_stale_h2_connection` uses an explicit message-based allowlist instead of the broad `is_closed()` check, preventing incorrect retries of protocol errors.
### gRPC
- Unary, client-streaming, server-streaming, and duplex flows are covered.
- JSON and protobuf byte paths exist, with unary protobuf-byte coverage now
present on both HTTP/2 and HTTP/3.
- Server-streaming unary JSON coverage now exists on both HTTP/2 and HTTP/3.
- Client-streaming unary JSON coverage now exists on both HTTP/2 and HTTP/3.
- Client-streaming protobuf-byte coverage now exists on both HTTP/2 and HTTP/3.
- gzip request/response handling is covered.
- `grpc-timeout` header emission is implemented.
- Strict `content-type` validation is implemented.
- Binary metadata send helpers are implemented via `metadata_bin(...)`.
- `grpc-status-details-bin` parsing is implemented.
- Metadata and trailer metadata accessors now exist for unary, streaming, and duplex APIs.
- Binary metadata round-trip coverage now exists for both HTTP/2 and HTTP/3
unary request paths, and duplex binary metadata/trailer metadata coverage now
exists on both HTTP/2 and HTTP/3.
- gRPC trailers-only error responses and streaming trailing-error status are
now covered on both HTTP/2 and HTTP/3.
- `decode_grpc_binary_header` supports unpadded base64 (standard base64 without trailing `=` padding).
### HTTP/3
- HTTP/3 request execution and connection reuse exist.
- gRPC over HTTP/3 has coverage.
- Reused HTTP/3 connections now retry safe requests after GOAWAY.
- Idle HTTP/3 connections that have received GOAWAY are now evicted from the
pool immediately instead of lingering until idle-timeout expiry, so they do
not block `max_idle_per_host` slot reuse.
- HTTP/3 GOAWAY handling now classifies unsent requests and in-flight streams
at or above the advertised GOAWAY identifier as stale-connection failures,
matching the retry semantics already enforced for HTTP/2.
- HTTP/3 peer-close handling now classifies requests that had not yet received
any response headers as stale-connection failures, so a reused QUIC
connection that dies before the next response starts can transparently retry
safe requests on a fresh connection.
- Idle peer-closes are now covered too: if the server closes a quiescent QUIC
connection between requests, the next request opens a fresh connection
instead of attempting to reuse the stale pooled handle.
- HTTP/3 retry classification now stops at response start: once response
headers have been observed, later connection loss remains a transport error
and is not silently retried on a fresh connection.
- Local HTTP/3 request-body stream failures are now isolated to the affected
request stream. A body producer error resets only that stream and leaves the
underlying QUIC connection reusable for the next request.
- `run_connection_task` sets `closed=true` before calling `fail_all`, eliminating a race where a concurrent request could receive a closed connection handle and hang.
- `should_retry_stale_h3_connection` no longer triggers on a broad `is_closed()` check; it uses the same allowlist approach as HTTP/2 to avoid retrying genuine protocol errors.
### TLS / ALPN
- `effective_alpn_protocols` for `Http3Only` mode now correctly returns `["h3"]` instead of an empty slice, ensuring QUIC connections advertise the right protocol.
### Client / Middleware
- `cookie_jar()` and `cookie_store()` on `ClientBuilder` no longer push a `CookieMiddleware` immediately; a single `CookieMiddleware` is injected at `build()` time, preventing duplicate cookie processing on every request.
## Resolved Issues
1. HTTP/2 keepalive timeout path is now correct.
- `check_keepalive_timeout()` now eagerly marks the connection closed before returning the error, preventing a race where the pool could hand out the stale handle to a concurrent request.
- `run_connection_task` (and `run_connection_task_with_initial_stream`) now set `closed=true` **before** calling `fail_all`, so any caller that wakes from `response_rx.recv()` sees the connection as closed and can enter the stale-connection retry path.
- `start_stream` write failures now also eagerly close the shared connection before failing pending commands.
- `should_retry_stale_h2_connection` was tightened: it uses an explicit message-based allowlist for stale-connection errors (including `"keepalive ping timed out"`) rather than a broad `is_closed()` shortcut that incorrectly retried protocol errors such as DATA-before-headers.
- Test `opens_new_http2_connection_after_keepalive_ping_timeout` now configures a short keepalive (30 ms idle / 30 ms ack timeout) so the PING fires and times out before the second request arrives.
2. HTTP/2 stale-connection health propagation is now correct.
- `closed` is set eagerly (before waking waiters) for all failure modes: keepalive timeout, task error, write failure.
- Pool eviction works correctly: `can_accept_new_stream()` checks `is_closed()`, so stale connections are never returned by `acquire()`.
3. HTTP/2 keepalive implementation is now production-quality for the core timeout/retirement path.
4. HTTP/3 connection-closure race condition eliminated (C-1).
- `closed.store(true)` now happens before `fail_all()` in `run_connection_task`.
5. HTTP/3 stale-connection retry logic tightened (C-2).
- `should_retry_stale_h3_connection` no longer uses a broad `is_closed()` shortcut.
6. gRPC binary header decoding hardened (C-3).
- `decode_grpc_binary_header` now handles both padded and unpadded base64.
7. Proxy CONNECT handshake fixed (H-1).
- Triple `\r\n` bug corrected to a single terminating `\r\n`.
8. HTTP/2 `SETTINGS_ENABLE_PUSH` handling corrected (H-2).
- Server-sent `SETTINGS_ENABLE_PUSH=0` is now a no-op instead of a protocol error.
9. HTTP/2 max frame size checked before allocation (H-3).
- `read_frame` validates the frame size against `SETTINGS_MAX_FRAME_SIZE` before allocating the buffer, preventing potential OOM on malformed frames.
10. `FrameType::Unknown` serialization corrected (H-4).
- Now panics with `unreachable!()` instead of silently emitting a zero byte.
11. Protocol version fallback now restricted to transport/timeout errors (H-5, H-6).
- `PreferHttp3` and `PreferHttp2` fallback paths only trigger on `Transport` or `Timeout` errors, not on application-level protocol errors.
12. SOCKS5 timeouts applied to all I/O (M-1).
- All reads and writes in the SOCKS5 handshake are now wrapped with `with_timeout_io`.
13. Redirect cookie stripping implemented (M-2).
- `sanitize_redirect_headers` strips the `cookie` header on cross-origin redirects.
14. H2 background task spawn fixed (M-3).
- Keepalive background tasks use `std::thread::spawn` + `async_io::block_on` instead of the non-existent `async_io::spawn`.
15. H2 stale-connection detection messages narrowed (M-4).
- Error-message matching uses precise substrings rather than broad patterns to avoid false positives.
16. `locally_reset_streams` map is now bounded (M-6).
- Entries are evicted once the map exceeds `MAX_LOCALLY_RESET`, preventing unbounded memory growth.
17. Double `CookieMiddleware` eliminated (L-1).
- Exactly one middleware is injected at `build()` time.
18. Proxy CONNECT accepts `HTTP/2 200` (L-2).
- The CONNECT response parser accepts both HTTP/1.1 and HTTP/2 status lines.
19. `Http3Only` ALPN protocols corrected (L-6).
- Returns `["h3"]` instead of an empty slice.
20. WebSocket DNS resolution is now async (L-3).
- The WebSocket client path now uses the shared async DNS cache and
configuration flow instead of blocking `ToSocketAddrs`.
21. WebSocket masking now uses fresh random bytes (L-4).
- The deterministic masking-key LCG was replaced with real randomness.
22. H2 write failure tears down the shared connection (L-5).
- `start_stream` now fails the whole connection on HEADERS write failure
instead of leaving in-flight streams stranded.
23. H2 keepalive no longer fragments the pool (M-7).
- Keepalive configuration is no longer part of `PoolKey`.
24. `base64_encode` is now shared (M-8).
- The duplicated helper was extracted into a single utility.
25. HTTP CONNECT request lines are now valid.
- The proxy tunnel builders now emit `CONNECT host:port HTTP/1.1` instead
of omitting the HTTP version token.
26. HTTP/2 HPACK header compression now follows peer table-size updates.
- The client no longer falls back to a global "indexed vs non-indexed"
switch based on the peer table size.
- A stateful HPACK encoder now tracks the peer's effective dynamic table
limit, emits header-block table-size updates when that limit changes, and
preserves dynamic-table reuse for entries that still fit.
- Oversized entries are still encoded safely, but they are not reused on
subsequent requests once the peer table is too small to retain them.
27. Idle HTTP/3 GOAWAY connections no longer block pool reuse.
- Once an HTTP/3 connection has received GOAWAY and gone quiescent, it is
evicted from the pool immediately rather than occupying an idle slot until
timeout expiry.
- This prevents stale non-reusable connections from starving
`max_idle_per_host` and forcing unnecessary fresh QUIC handshakes.
28. HTTP/3 GOAWAY stale-request classification tightened.
- Requests still pending/opening when GOAWAY arrives are now failed as
`StaleConnection`, not generic transport errors.
- In-flight streams whose IDs are rejected by the GOAWAY identifier are
also classified as stale so the existing transparent retry path can
safely re-dispatch idempotent work on a fresh QUIC connection.
29. HTTP/3 peer-close stale-request classification tightened.
- If a QUIC connection closes before a request has received any response
headers, pending/opening work and response-less in-flight streams are now
failed as `StaleConnection`.
- This preserves transparent retry for safe requests when a reused HTTP/3
connection dies before the next response begins, instead of surfacing a
generic transport error.
30. HTTP/3 retry semantics now stop once a response has started.
- Mid-response connection loss is intentionally kept on the transport-error
path rather than being reclassified as stale.
- This prevents silent replay once response headers have already been
observed, while still allowing the next explicit request to recover on a
fresh QUIC connection.
31. HTTPS proxy tunnel coverage now includes HTTP/2 end-to-end.
- The integration suite now exercises HTTP/2 requests over proxied HTTPS
tunnels for both:
- HTTP CONNECT
- SOCKS5
- This closes a real test-matrix gap where proxy interop had only been
validated on the HTTP/1.1 request path.
32. Local HTTP/3 upload-stream errors no longer poison the whole connection.
- A request-body stream error raised by the local producer is now handled
as a per-stream failure rather than bubbling out of the H3 connection
task and tearing down unrelated work.
- The client now closes only the affected stream with
`H3_REQUEST_CANCELLED`, surfaces the original transport error to that
request, and keeps the QUIC connection reusable for a subsequent request
on the same connection.
## Open Issues
### Medium Priority
- gRPC still needs broader interop coverage.
- Core functionality is much stronger now, but broader ecosystem interop and metadata edge cases still need more validation.
### Low Priority
- Proxy tunnel interop beyond the current localhost coverage would still be useful.
- The new regression suite covers the main auth/no-auth tunnel paths, but it
is still local test infrastructure rather than broader real-proxy interop.
## Suggested Next Steps
1. gRPC broader interop coverage.
2. Broader real-proxy interoperability checks.
3. Optional external QUIC / HTTP/3 interoperability probes beyond the current localhost harness.
## Useful Existing Regressions
- `reuses_http2_connection_for_multiple_requests`
- `reuses_http2_connection_after_keepalive_ping_ack`
- `does_not_reuse_http2_connection_across_different_keepalive_configs`
- `retries_http3_request_after_goaway_on_reused_connection`
- `retries_http3_request_after_peer_closes_reused_connection_before_response`
- `opens_new_http3_connection_after_idle_peer_close`
- `does_not_retry_http3_request_after_response_headers_have_started`
- `evicts_idle_http3_goaway_connection_before_reusing_pool_slot`
- `request_body_stream_error_fails_only_the_failed_http3_stream`
- `protocol::http3::tests::goaway_rejects_stream_ids_at_or_above_identifier`
- `protocol::http3::tests::goaway_unsent_request_errors_are_stale`
- `protocol::http3::tests::goaway_rejected_stream_errors_are_stale`
- `protocol::http3::tests::peer_close_unsent_request_errors_are_stale`
- `protocol::http3::tests::peer_close_before_response_errors_are_stale`
- `grpc_http2_binary_metadata_round_trips_through_helpers`
- `grpc_http3_binary_metadata_round_trips_through_helpers`
- `streaming_grpc_http3_response_surfaces_trailing_error_status`
- `exposes_grpc_error_status_from_trailers_only_response_over_http3`
- `executes_grpc_client_streaming_json_request_over_http3`
- `executes_grpc_client_streaming_protobuf_bytes_request_over_http2`
- `executes_grpc_client_streaming_protobuf_bytes_request_over_http3`
- `executes_grpc_protobuf_bytes_request_over_http3`
- `reads_grpc_server_streaming_messages_over_http3`
- `grpc_duplex_call_supports_interleaved_send_and_receive_over_http2`
- `grpc_duplex_call_supports_interleaved_send_and_receive_over_http3`
- `sse_last_event_id_flows_end_to_end_over_http2`
- `opens_new_http2_connection_after_keepalive_ping_timeout`
- `accepts_http2_settings_enable_push_from_server`
- `header_codec_reuses_dynamic_table_with_small_peer_table`
- `header_codec_emits_dynamic_table_size_update_when_peer_size_changes`
- `header_codec_does_not_reuse_entry_larger_than_peer_table`
- `http_connect_proxy_tunnels_https_without_auth`
- `http_connect_proxy_tunnels_https_with_auth`
- `http_connect_proxy_tunnels_https_over_http2`
- `socks5_proxy_tunnels_https_without_auth`
- `socks5_proxy_tunnels_https_with_auth`
- `socks5_proxy_tunnels_https_over_http2`