setono/http-client-bundle
Setono HttpClientBundle integrates a configurable HTTP client into Symfony applications, providing an injectable service for making outbound requests with centralized configuration, sensible defaults, and clean dependency injection.
First, the framing: this is a Symfony bundle, not a Laravel package. There is no service provider, no auto-discovery, no config/http_client.php. If you are working in a Laravel codebase you will be hand-wiring everything it does, and in most cases you should just skip it and use symfony/http-client (or Laravel's Http facade) directly. The bundle is archived, has 1 star, 0 dependents, and its last release was June 2022 — treat it as vendor code you are adopting, not a dependency you are consuming.
That said, if you are in a Symfony app (or a Laravel app that already pulls in Symfony components), the shape of the package is:
src/DependencyInjection/Configuration.php and src/DependencyInjection/SetonoHttpClientExtension.php in the repo. The entire public API of a bundle like this is its config tree plus the service IDs it registers. Roughly 10 minutes of reading tells you everything; the docs (if any) will be thinner than the code.composer require setono/http-client-bundle
Then register it explicitly in config/bundles.php:
Setono\HttpClientBundle\SetonoHttpClientBundle::class => ['all' => true],
base_uri and auth headers at every call site":
# config/packages/setono_http_client.yaml
setono_http_client:
clients:
acme:
base_uri: 'https://api.acme.test/v1/'
headers:
Accept: 'application/json'
Exact key names must be confirmed against Configuration.php — this is the part most likely to have drifted.Symfony\Contracts\HttpClient\HttpClientInterface (or the scoped/named alias the bundle exposes) into a service and make one ->request('GET', 'widgets') call. If that round-trips with the correct host and headers, you have validated the bundle. If it does not, stop — you have lost nothing, because the underlying ScopingHttpClient does the same job in three lines.Laravel translation if you insist on it: in AppServiceProvider::register(), bind HttpClientInterface to Symfony\Component\HttpClient\HttpClient::create([...defaults...]), and register one named binding per upstream API. You get 95% of the value with none of the abandoned-bundle risk.
Named client per upstream, injected by alias. The pattern the bundle is built around is one configured client per third-party service, resolved by name rather than by constructing options inline. In Symfony this is autowiring by argument name or a service alias; in Laravel, contextual binding:
$this->app->when(AcmeGateway::class)
->needs(HttpClientInterface::class)
->give(fn () => HttpClient::create([
'base_uri' => config('services.acme.base_uri'),
'headers' => ['Accept' => 'application/json'],
]));
Note the trailing slash on base_uri — Symfony resolves relative paths against it, and https://api.acme.test/v1 without the slash will drop v1 when you request widgets.
Wrap the client in a gateway class, always. Do not let HttpClientInterface leak into controllers or jobs. The bundle gives you a configured transport; your domain code should see AcmeGateway::listWidgets(): array. This is what makes the package replaceable — when you inevitably rip it out, you change one constructor.
Layer auth and retries as decorators, not config. Symfony's HTTP client composes well: ScopingHttpClient, RetryableHttpClient, TraceableHttpClient. Rather than hunting for whether this bundle supports retry config (it probably does not, given its age), decorate:
$client = new RetryableHttpClient(
new ScopingHttpClient($inner, $scopes, 'https://api.acme.test'),
maxRetries: 2
);
This is the extension point that survives the bundle being archived.
Mock at the transport, not the gateway. Symfony's MockHttpClient with a MockResponse queue is the idiomatic test double; it asserts on URL, method, and body without touching the network. In Laravel, Http::fake() covers the Http facade but not an injected HttpClientInterface, so if you adopt this bundle you must test with MockHttpClient and lose Http::fake()'s ergonomics. That is a real day-to-day cost.
Configuration-driven environment switching. Keep base_uri in config per environment (sandbox vs. production) so no code path hardcodes a host. With the bundle this is the YAML file; without it, config/services.php + a binding.
composer.json's symfony/* constraints before you require it — a 2022 bundle very likely pins ^4.4|^5.0 and will fight a modern Symfony or a Laravel that ships Symfony 7 components. Composer will tell you immediately; believe it.extra.laravel block. You must register the bundle manually and you are running a Symfony bundle inside Laravel's container, which means Kernel-style bundle bootstrapping does not happen for free. Expect to either use a bundle-bootstrapping shim or give up.bin/console cache:clear (or at least warm the container) — a stale compiled container will silently keep old base_uri values and produce baffling "wrong host" bugs.$options merge over configured defaults; arrays like headers merge rather than replace in some paths but not others. When a header "disappears," dump the resolved options before debugging the remote service.getContent()/getStatusCode(). A bundle that returns a client gives you no protection here — always call getStatusCode() explicitly or wrap in a gateway that does.symfony/http-client's data collector, not this bundle) is the fastest path. In Laravel there is no such panel — use 'debug' => true on the client plus LoggerInterface decoration, or TraceableHttpClient.ScopingHttpClient yourself; (2) decorate the injected client; (3) subclass/replace the bundle's client factory service definition; (4) fork. Given 1 star and an archived status, options 1–2 are the only ones with a sane maintenance story.app/Support/ is legally trivial — and probably the right move if you actually want its config ergonomics.How can I help you explore Laravel packages today?