Laravel HTTP Client: Named Instances &amp; Middleware Stacks | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel Contextual HTTP Clients: Per-Service Config, Retries, and Middleware Stacks

 Laravel Contextual HTTP Clients: Per-Service Config, Retries, and Middleware Stacks
====================================================================================

 Stop scattering HTTP client config across service classes. Learn how to build named, pre-configured HTTP client instances with retry policies, middleware, and per-service authentication baked in at the container level.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 17 Jun 2026 · Updated 17 Jun 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Contextual HTTP Clients: Per-Service Config, Retries, and Middleware Stacks](https://cdn.msaied.com/223/2706e56a293bb3b59f39b52956efac09.png) 

  On this page +1. [The Problem With Ad-Hoc HTTP Clients](#the-problem-with-ad-hoc-http-clients)
2. [Registering a Named Client in a Service Provider](#registering-a-named-client-in-a-service-provider)
3. [Injecting Named Clients Into Services](#injecting-named-clients-into-services)
4. [Testing Without Hitting the Network](#testing-without-hitting-the-network)
5. [Retry Policies Worth Knowing](#retry-policies-worth-knowing)
6. [Takeaways](#takeaways)

 The Problem With Ad-Hoc HTTP Clients
------------------------------------

Most Laravel codebases scatter HTTP configuration everywhere. One service sets a base URL, another configures a timeout, a third adds an auth header — and none of it is testable in isolation. Laravel's `Http` facade is powerful, but without structure it becomes a maintenance liability.

The solution is to register **named, fully-configured HTTP client instances** through the service container, so every collaborator receives a ready-to-use client with the correct base URL, headers, retry policy, and middleware already applied.

Registering a Named Client in a Service Provider
------------------------------------------------

`Http::buildClient()` returns a `PendingRequest`, which is itself a fluent builder. You can bind one per external service:

```php
// app/Providers/HttpClientsServiceProvider.php

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Http;

class HttpClientsServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton('http.stripe', function () {
            return Http::baseUrl(config('services.stripe.base_url'))
                ->withToken(config('services.stripe.secret'))
                ->timeout(15)
                ->retry(3, 200, function (\Exception $e) {
                    return $e instanceof \Illuminate\Http\Client\ConnectionException;
                })
                ->withMiddleware($this->idempotencyMiddleware());
        });

        $this->app->singleton('http.sendgrid', function () {
            return Http::baseUrl('https://api.sendgrid.com/v3')
                ->withHeader('Authorization', 'Bearer ' . config('services.sendgrid.key'))
                ->acceptJson()
                ->timeout(10)
                ->retry(2, 500);
        });
    }

    private function idempotencyMiddleware(): callable
    {
        return function (callable $handler) {
            return function ($request, array $options) use ($handler) {
                $request = $request->withHeader(
                    'Idempotency-Key',
                    (string) str()->uuid()
                );
                return $handler($request, $options);
            };
        };
    }
}

```

The middleware closure follows the Guzzle handler stack contract — Laravel's `PendingRequest::withMiddleware()` accepts any PSR-compatible Guzzle middleware.

Injecting Named Clients Into Services
-------------------------------------

Use contextual binding or a typed wrapper to keep service classes clean:

```php
// app/Services/StripeClient.php

use Illuminate\Http\Client\PendingRequest;

final class StripeClient
{
    public function __construct(
        private readonly PendingRequest $client
    ) {}

    public function createPaymentIntent(int $amountCents, string $currency): array
    {
        return $this->client
            ->post('/payment_intents', [
                'amount'   => $amountCents,
                'currency' => $currency,
            ])
            ->throw()
            ->json();
    }
}

```

Bind it contextually so the container knows which `PendingRequest` to inject:

```php
// Inside HttpClientsServiceProvider::register()

$this->app->when(StripeClient::class)
    ->needs(PendingRequest::class)
    ->give(fn ($app) => $app->make('http.stripe'));

```

Now `StripeClient` has zero configuration knowledge. It just calls endpoints.

Testing Without Hitting the Network
-----------------------------------

Laravel's `Http::fake()` intercepts all outgoing requests regardless of how the client was built, so your named instances are fully fakeable:

```php
// tests/Feature/StripeClientTest.php

use Illuminate\Support\Facades\Http;
use App\Services\StripeClient;

it('creates a payment intent', function () {
    Http::fake([
        '*/payment_intents' => Http::response([
            'id'     => 'pi_test_123',
            'status' => 'requires_payment_method',
        ], 201),
    ]);

    $result = app(StripeClient::class)->createPaymentIntent(5000, 'usd');

    expect($result['id'])->toBe('pi_test_123');

    Http::assertSent(fn ($request) =>
        $request->url() === config('services.stripe.base_url') . '/payment_intents'
        && $request['amount'] === 5000
    );
});

```

Because the `PendingRequest` is resolved fresh per singleton scope, `Http::fake()` intercepts correctly. If you need per-test isolation, swap the singleton for a `bind` or reset the fake between tests.

### Retry Policies Worth Knowing

- `retry(3, 200)` — three attempts, 200 ms fixed delay.
- Pass a `throw` boolean as the fourth argument (`false`) to suppress the final exception and return the last response instead.
- The callback form lets you retry only on specific exception types or response status codes, avoiding retries on 4xx client errors.

Takeaways
---------

- Register one named `PendingRequest` singleton per external service in a dedicated service provider.
- Use contextual binding to inject the correct client into each service class without string-keyed `app()` calls.
- Guzzle middleware added via `withMiddleware()` is the right place for cross-cutting concerns like idempotency keys, request signing, or structured logging.
- `Http::fake()` works transparently with named instances — no extra test infrastructure needed.
- Prefer `->throw()` on the response chain over manual status checks; it raises `RequestException` with the full response attached.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [http-client](https://www.msaied.com/public/articles?search=http-client)
- [service-container](https://www.msaied.com/public/articles?search=service-container)
- [testing](https://www.msaied.com/public/articles?search=testing)

 Frequently asked questions 
---------------------------

  Does Http::fake() intercept requests made through a named PendingRequest singleton?Yes. Laravel's fake layer patches the underlying Guzzle handler factory globally, so any PendingRequest instance — regardless of how it was constructed — will be intercepted when Http::fake() is active in a test.

   Should I use a singleton or a transient binding for named HTTP clients?Singleton is fine for stateless configuration (base URL, headers, timeout). If your middleware mutates per-request state — such as generating a unique idempotency key — ensure the mutation happens inside the middleware closure at request time, not at binding time, so the singleton remains safe.

   Can I add structured logging to every outgoing request without touching each service class?Yes. Add a Guzzle middleware via withMiddleware() in the service provider. The middleware receives the request before it is sent and the response after, giving you a single place to log URL, duration, and status code for every call made through that named client.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleLaravel Signed Routes and Temporary URLs: Secure Link Patterns Beyond the Basics](https://www.msaied.com/public/articles/laravel-signed-routes-and-temporary-urls-secure-link-patterns-beyond-the-basics) [Next articleLaravel Octane + FrankenPHP: Persistent State, Shared Services, and Safe Bootstrapping](https://www.msaied.com/public/articles/laravel-octane-frankenphp-persistent-state-shared-services-and-safe-bootstrapping)  

   On this page
-------------

1. [The Problem With Ad-Hoc HTTP Clients](#the-problem-with-ad-hoc-http-clients)
2. [Registering a Named Client in a Service Provider](#registering-a-named-client-in-a-service-provider)
3. [Injecting Named Clients Into Services](#injecting-named-clients-into-services)
4. [Testing Without Hitting the Network](#testing-without-hitting-the-network)
5. [Retry Policies Worth Knowing](#retry-policies-worth-knowing)
6. [Takeaways](#takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
