symfony/http-client
Symfony HttpClient provides powerful methods to fetch HTTP resources synchronously or asynchronously, with a clean API for building requests, handling responses, streaming, and testing HTTP calls in PHP applications.
Architecture fit This is a framework-agnostic Symfony Component, not a Laravel package. It ships no service provider, no config file, and no facade — so "fit" is a decision about whether to introduce a second outbound HTTP stack next to the one Laravel already wraps (Guzzle). It is a strong fit, but only under specific conditions:
Symfony\Contracts\HttpClient\HttpClientInterface in your domain services and bind it in a provider. This gives you MockHttpClient for tests, decorators for logging/metrics/tracing, and a clean seam to swap implementations later. This is the highest-value, lowest-risk pattern.Psr18Client/HttplugClient let you satisfy third-party SDKs that type-hint Psr\Http\Client\ClientInterface (Stripe, AWS, most AI SDKs) without rewriting call sites. You get the swap without the migration.curl_multi (AsyncResponse, stream()), NoPrivateNetworkHttpClient as a first-class SSRF guard for user-supplied URLs (webhooks, URL previews, image imports), ScopingHttpClient for per-host base URI/timeout policy, RetryableHttpClient with backoff, and streaming with progress callbacks.Http::withToken(...)->post(...) ergonomics. This component is lower-level; you will be writing and maintaining a thin wrapper, and that wrapper is your real product surface.Data-quality flags for diligence: Dependents: 0 and 2,029 stars are artifacts of this being a read-only split of symfony/symfony. Real adoption is not represented here. Do not treat these as adoption signals in either direction, and do not treat the star count as a maturity proxy.
Integration feasibility High, with caveats.
composer require symfony/http-client plus symfony/http-client-contracts; PSR-18 use additionally needs psr/http-client and a PSR-7 implementation (nyholm/psr7 is the light choice).CurlHttpClient requires ext-curl and is the production default (proxy, HTTP/2/3, connection reuse); NativeHttpClient requires allow_url_fopen and has a materially weaker edge-case record in the changelogs (dechunk filter replacement, proxy handling, truncated streams). Pick curl and pin it.Technical risk
CVE-2026-48736 (IPv6 transition forms bypassing NoPrivateNetworkHttpClient) is patched across 5.4, 6.4, 7.4 and 8.1. If SSRF protection is your reason for adopting this component, an unpatched pin gives you false assurance — worse than no guard, because teams stop reviewing URLs. Pin to patched versions and assert it in CI.^7.4.CachingHttpClient was reworked for RFC 9111 in 7.4 and has accumulated follow-on fixes (trusted hosts, header replacement, ordering after UriTemplate/Scoping). Treat as beta-grade; do not put it in a hot path without a fallback.AsyncResponse::stream() consumption guards, __destruct() throwing, abandoned-wrapper behavior, and first-chunk re-yielding after reconnect. Concurrency bugs here surface as hangs, leaked sockets, or silent short reads — not clean exceptions.max_duration/body limits at the application layer.symfony/symfony, not this repo. Your security scanner, issue-link tooling, and changelog automation will point at the wrong place.curl is mechanically possible behind your interface.Key questions
MockHttpClient for new code and leave Guzzle MockHandler in place for legacy, or converge?Stack fit
Framework-agnostic component; Laravel integration is entirely yours to build. Recommended shape: an OutboundHttp port interface in your domain layer, a Laravel service provider binding HttpClientInterface to a decorated CurlHttpClient, and a single factory for scoped clients. Do not attempt to rewire Laravel's Http facade — it is Guzzle-shaped and you will fight it. Add Psr18Client alongside so third-party SDKs can be pointed at the same transport and inherit the same timeout/retry/observability policy.
Migration path Sequence to maximize value per unit of risk:
NoPrivateNetworkHttpClient wrap for any user-supplied URL path; timeouts, max_redirects, per-host concurrency caps, and jittered retry in one decorator.HttpClientInterface with MockHttpClient tests.Compatibility
Pin the Symfony minor line to your PHP floor; keep psr/http-client and a single PSR-7 implementation; require ext-curl in composer.json and fail fast if absent. If you run Laravel Octane or long-lived workers, explicitly review CurlHttpClient instance reuse and reset() semantics across requests — sharing client state between requests is a documented regression area in the 6.4/7.3 patch lines.
Sequencing Security pin → observability decorator → policy (timeouts/retries/caps) → PSR-18 swap → async adoption → caching. Adopting async before observability is the common failure: you get concurrent requests and no way to see which one hung.
Maintenance High patch cadence across four maintained branches (5.4, 6.4, 7.4, 8.1). Automate dependency PRs, but gate merges on a changelog read — regressions and reversals are visible in release notes and invisible in semver. Pin exact patch versions in production lockfiles; float only in staging. Track the monorepo release feed, not this repo's. MIT with no vendor SLA: you own the failure.
Support Documentation is good (symfony.com component docs are more complete than the repo README, which is a stub pointing outward). Community support via Symfony channels is deep but not contractual. The realistic support model is: your platform team owns the wrapper, the policy layer, and the upgrade cadence; upstream owns the transport internals. Plan for at least one engineer who can read the component source — debugging async and stream behavior from docs alone is not realistic.
Scaling
curl_multi delivers real wins on fan-out: N parallel upstream calls at p99 of the slowest rather than the sum. Two scaling hazards: per-host connection caps (regressed twice in patch history — verify max_host_connections behavior after every bump) and long-lived worker state under Octane, where connection reuse and abandoned AsyncResponse objects become memory and socket-leak surfaces. Enforce per-host concurrency ceilings at the decorator level so one vendor's degradation cannot exhaust your FPM pool.
Failure modes
TransportExceptionInterface → 500s; map to domain errors at the port boundary, always.max_redirects; cross-authority auth-header behavior has changed twice, so never rely on implicit stripping for secret-bearing requests — scope credentials per host.NoPrivateNetworkHttpClient gives silent SSRF exposure; assert patched version in CI.Ramp-up
A competent PHP engineer is productive in 1–2 days for synchronous use; async, streaming, and decorator composition take roughly a sprint to get right. The real organizational cost is idiom divergence: Laravel developers will reach for Http:: by reflex, so the port must be the path of least resistance (helper function, facade alias, or documented example in the repo). Budget one sprint for standards, the shared wrapper, decorator stack, and test doubles — and treat the "will we fully migrate off Guzzle?" question as a decision to make now, not later. A permanently unconsolidated dual stack is the most likely negative outcome of this adoption, and it is entirely a policy choice rather than a technical constraint.
How can I help you explore Laravel packages today?