ugi 0.2.1

Runtime-agnostic Rust request client with HTTP/1.1, HTTP/2, HTTP/3, H2C, WebSocket, SSE, and gRPC support
Documentation
# 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`