How to Change Laravel's HTTP Client default behaviour cover image

How to Change Laravel's HTTP Client default behaviour

Tim Geisendörfer • January 16, 2022

laravel http-client dependency-injection

When using Laravel packages which are relying on Laravel's HTTP Client you sometimes want to change the behaviour how the package is interacting with external Services. One of the ways to do that is using the powerful dependency injection feature brought by the Laravel service container.

The problem

If you only want to see some code examples you can skip this section. But I always love to give you some background information.

In one of our projects we had to consume an API from a document management software called DocuWare. When you have to do such things it's always worth a try to Google around searching for some existing packages out there. The Laravel ecosystem is insanely huge so someone out there might had to solve the same problems in the past. And if he is nice he has published his source code.

Indeed, we found the Laravel Docuware package by a company called CodeBar AG (thank you for that). After source diving the package it seemed to fit our needs. So we gave it a try and installed it in a fresh Laravel installation. After tinkering around with it We found a problem regarding our capability to use this package.

The DocuWare REST API service which has to be consumed by our application sits behind a firewall in the Customers local area network. Since this document management system handles pretty sensitive data it would be a security vulnerability to expose this system to the whole internet. So only authorized systems should pass this firewall by predefined rules. The Customers network administrator told us that we need to send some more information / credentials packed into the HTTP header with every request sent to the firewall. When the credentials are correct the firewall system lets our requests pass through to the DocuWare system. And everything should work fine.

Of course the Laravel DocuWare package did not have a mechanism to send such additional header information on every request. This is a really special use case and on 100 different customer implementations you might have 100 different solutions. You cannot build a package to match all of those cases. So we had to find a solution for this problem. We found different approaches to solve this problem - here are some of them.

We could drop the idea to use an existing package and build our own implementation of the DocuWare REST API. This might be the most flexible but also most time-consuming way.

Second we could fork the existing package and adapt it to our needs. This is flexible and not that time-consuming but when doing that you are responsible for the whole codebase. You have to be aware of possible upcoming security problems or future migrations in a source code which you might not fully understand on every point.

Third you can try to extend some of your applications core components with your specific logic so the original package can be used without modification. And that's what we are going to do. The key word at this point is the powerful Laravel feature called dependency injection which is brought by the service container.

How to extend the Laravel HTTP Client

So let's do some code! Due to our package source diving we know that the package is using the Laravel HTTP Client which was added to the framework since Laravel 8.0 under the hood. Let's assume that we have to pass some additional headers on every request which is done by the HTTP Client. In our case we are using a simplified way of a microservice architecture and the service we are going to build can be called as "DocuWare API service".

Like many other Laravel core components the HTTP client uses the Factory pattern under the hood. So everytime a new Pending Request instance has to be created this job is done by a class called HttpClientFactory. And since we want to change every request which is sent by our API microservice the HttpClientFactory is the right place to change all of our applications HTTP requests. So let's create our own HttpClientFactory implementation.

At first create a new class at app/Factories/HttpClientFactory.

namespace App\Factories;

use Illuminate\Http\Client\Factory;

class HttpClientFactory extends Factory
{
    /**
     * Execute a method against a new pending request instance.
     *
     * @param string $method
     * @param array $parameters
     * @return mixed
     */
    public function __call($method, $parameters)
    {
        if (static::hasMacro($method)) {
            return $this->macroCall($method, $parameters);
        }

        return tap($this->newPendingRequest(), function ($request) {
            $request
            //This line has changed from the base factory class
                ->withBasicAuth(config('services.firewall.login'), config('services.firewall.password'))
                ->stub($this->stubCallbacks);
        })->{$method}(...$parameters);
    }
}

Within this HttpClientFactory class we are extending the framework's original Factory class. Since we want to extend every request from the HTTP Client the magic __call method is the right place to do that. Most of that code is copied from the base Factory class. With only one line of code we have added a basicAuth Authorization Header to every HTTP Request. It's just simple as that.

Now that we have created our own HTTP Client factory class we have to inform the Laravel service container that we want to use this factory instead of the original one for all of our HTTP Client instances. This is done in our AppServiceProvider's boot method:

/**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->app->bind(Illuminate\Http\Client\Factory::class, function ($app) {
            return new App\Factories\HttpClientFactory($app->make(Dispatcher::class));
        });
    }

With these two lines of code we are binding our new Factory class to the service container. So everytime when the application requests a new HttpClientFactory instance from the service container our own Factory implementation will be used. Under the hood Laravel uses the Dependency Injection pattern for that.

Please take care that this new factory changes every single HTTP Client request in your whole application. Even those which you don't want to adapt. Since we are using a microservice at this point we can easily do that. In a classic monolithic architecture you have to be careful with these changes. In a monolithic app you might want to conditionally bind this new factory. A good way to conditionally bind a factory is by creating a new middleware which is only applied to some parts of your application.

Final Words

I know the Laravel application container or dependency injection feels like a kind of black magic for some people. And I can understand that. At first this seems pretty abstract and is really hard to understand for new developers. But I can tell you it is worth it to learn how to use it.

You can swap out almost every component of the Laravel framework with this technology. It is pretty powerful and using it makes you really flexible. And there are almost no limits in your doing. This flexibility is one of the main reasons why the Laravel Framework became so popular.

I always love to have some feedback you can contact me on Twitter or per mail if you want! Have a nice week!