Reuse HTTP transports across coroutines and complete cleanup

Merged
BI
4d ago
+407-162

The HTTP client now reuses its default transport across requests, and both default and named transports can serve asynchronous requests from concurrent coroutines. This uses the handler leasing in the updated Guzzle fork: each coroutine borrows a multi handler while it has work, then returns it for reuse.

Previously, unnamed requests rebuilt their transport and named asynchronous requests needed separate handlers. Pusher also maintained its own coroutine-local handler registry. Those paths can now use the fork's shared implementation.

Reuse without sharing request state

The factory keeps one default transport and one per named connection. Clients, middleware stacks and cookie jars remain request-owned. Headers, credentials, callbacks, options and cookies do not become attached to the first request that uses a transport.

Named connections and Pusher's client_options accept max_idle_multi_handlers alongside max_idle_handles. These control different resources: idle multi handlers and idle easy handles respectively. Active requests are not capped by either setting. The HTTP client and Pusher continue rejecting connection-cap options; coroutine concurrency remains the way to bound their work.

Pusher now uses the normal transport factory for synchronous and asynchronous requests. Its separate handler-context class is removed.

Keep DNS pins request-owned

Sharing a transport must not let one request's DNS pin change the destination of a later request. Requests carrying raw CURLOPT_RESOLVE pins therefore get an isolated transport, including pins added by middleware or by pre-send destination resolution.

Requests routed through a destination policy's proxy are also isolated. A previously opened proxy connection must not bypass the address that the policy just approved. Direct policy routes retain connection reuse when their approved addresses match.

Isolated transports preserve named connection settings. Required persistent sharing is rejected, and preferred persistent sharing falls back to handler-level sharing. Faked responses, caller-owned clients and handlers, and the streaming availability checks retain their behavior.

Complete coroutine cleanup

Coroutine shutdown now continues cancelling transport work and draining queued callbacks after a callback throws, then reports the first failure. Together with the Guzzle fork's native-work tracking, this also cleans up transfers whose response promises were settled directly.

Dependencies and validation

The root and split packages now require hypervel/guzzle:^8.3 and, where used directly, hypervel/guzzle-promises:^3.1. This PR depends on releases from:

Those releases must be available before a normal Composer install can resolve the new minimums. The forks' own replacement declarations continue to satisfy compatible third-party SDK requirements.

Formatting, static analysis, the HTTP suite and the affected Sentry, Telescope, AI, MCP, AWS and Pusher tests pass against copied candidate dependencies. Tests cover connection reuse across coroutines, request-state isolation, DNS pins, proxy routing, idle limits and cleanup failures. Existing streaming reuse assertions remain unchanged.

The HTTP and broadcasting docs describe the new behavior. The benchmark README is corrected to describe shared default transports; this PR adds no benchmark results or throughput claims.

[!NOTE]

Share HTTP transports across coroutines and isolate risky requests

  • Synchronous and asynchronous requests now reuse one cached default or named handler in Factory, so connections persist across coroutines instead of being rebuilt per mode (Factory.php).
  • Requests with raw DNS pins (CURLOPT_RESOLVE) and proxied destination-policy requests use isolated handlers, preventing pinned or proxy-approved routes from leaking into later requests (PendingRequest.php).
  • Adds max_idle_multi_handlers connection option for pooled idle multi-handler limits; rejected at request-option layers and validated as a non-negative integer.
  • Coroutine exit cleanup now runs remaining queued callbacks and cancels pending promises even after a callback throws, then rethrows the first failure (CoroutineState.php).
  • Pusher/Reverb broadcasters reuse the same handler for sync and async calls; the coroutine-local PusherHandlerContext class is deleted (BroadcastManager.php).
  • Requires Hypervel Guzzle >= 8.3 and Guzzle Promises >= 3.1 across all composer.json manifests.
  • Risk: connections configured with persistent-required transport sharing now throw InvalidArgumentException when an isolated handler is requested; check Factory.newIsolatedHandler and connectionHandlerOptions.

Macroscope summarized 5f5f9b4.

Summary by CodeRabbit

  • New Features
    • Default HTTP requests now reuse connections across synchronous and asynchronous operations, improving connection efficiency.
    • Added controls for limiting retained idle connections and multi-handlers.
  • Bug Fixes
    • Requests with DNS pins or proxied destination policies use isolated transports to prevent unsafe connection reuse.
    • Asynchronous operations handle promise cleanup more reliably, including when queued work raises an error.
  • Documentation
    • Updated HTTP client and broadcasting guidance to explain connection reuse, isolation, and idle-resource limits.