Skip to content

HTTP

This HTTP client uses the immutable objects describing the protocol from the innmind/http package.

Usage

use Innmind\HttpTransport\Success;
use Innmind\Http\{
    Request,
    Method,
    ProtocolVersion,
};
use Innmind\Url\Url;

$http = $os->remote()->http();

$request = Request::of(
    Url::of('https://github.com/'),
    Method::get,
    ProtocolVersion::v11,
);
$http($request)->match(
    static fn(Success $success) => var_dump(
        $success
            ->response()
            ->body()
            ->toString(),
    ),
    static fn(object $error) => throw new \RuntimeException(),
);

When sending an HTTP request it will return an Either<Failure|ConnectionFailed|MalformedResponse|Information|Redirection|ClientError|ServerError, Success>, where each of this classes are located in the Innmind\HttpTransport\ namespace. This type may be scarry at first but it allows you to use static analysis to deal with every possible situation (or not by throwing an exception like in the example). No more surprises of uncaught exceptions in production!

Info

Responses are wrapped in classes such as Success, Redirection, etc... to avoid confusion as a response can be on both sides of the Either. This way you now for sure a 2XX response is on the right side and the other ones on the left one.

You can specify headers on your requests like this:

use Innmind\Http\{
    Headers,
    Header\Header,
    Header\Value\Value,
};

$request = Request::of(
    Url::of('https://github.com/'),
    Method::get,
    ProtocolVersion::v11,
    Headers::of(
        new Header('User-Agent', new Value('your custom user agent string')),
    ),
);
Tip

innmind/http comes with a lot of header classes to simplify some common cases.

You can always specify a body like so:

use Innmind\Filesystem\File\Content;
use Innmind\Http\Header\ContentType;
use Innmind\Json\Json;

$request = Request::of(
    Url::of('https://your-service.com/api/some-endpoint'),
    Method::post,
    ProtocolVersion::v11,
    Headers::of(
        ContentType::of('application', 'json'),
    ),
    Content::ofString(Json::encode(['some' => 'payload'])), #(1)
);
  1. see innmind/json

Here we send some json but you can send anything you want.

The body of a Request, and a Response, is expressed via the Content class from the filesystem abstraction. This means that it can contain any valid file content.

You'll learn more on this Content in the next chapter.

Following redirections

By default the client returned by $os->remote()->http() doesn't follow redirections. In order to do so you need to decorate the client like this:

use Innmind\HttpTransport\FollowRedirections;

$http = FollowRedirections::of($os->remote()->http());

This decorator will follow to up to 5 redirections.

Info

Redirections are handled this way so you can compose all the decorators the way you need. For example you want to apply exponential backoff between each redirection.

Resiliency

We tend to think networks are always stable or services as always up, but at some point failures will happen. This abstraction comes with 2 strategies to deal with them.

Circuit breaker

If you need to call a service a lot but at some point becomes unavailable (for maintenance for example), you don't want to continue to try to call this service for a certain amount of time.

The circuit breaker is a pattern that will automatically return an error response (without doing the actual call) if the service failed in the previous x amount of time.

You apply this pattern via this decorator:

use Innmind\HttpTransport\CircuitBreaker;
use Innmind\TimeContinuum\Earth\Period\Second;

$http = CircuitBreaker::of(
    $os->remote()->http(),
    $os->clock(),
    Second::of(10),
);

The circuit breaks on a per domain name logic.

Info

In case a circuit is open then the error response will be a 503 Service Unavailable with a custom header X-Circuit-Opened so you can understand who responded.

Retry with exponential backoff

When a call fail you can automatically retry the call after a certain amount of time. You can apply the retries like this:

use Innmind\HttpTransport\ExponentialBackoff;

$http = ExponentialBackoff::of(
    $os->remote()->http(),
    $os->process()->halt(...),
);

This will retry all errors 5XX responses and connection failures at most 5 times and will wait 100ms, 271ms, 738ms, 2s and 5.4s between each retry.

Tip

You can improve the resiliency of the whole operating system abstractions like this:

use Innmind\OperatingSystem\OperatingSystem\Resilient;

$os = Resilient::of($os);

Even though for now it only applies this strategy to the HTTP client, you future prood yourself by using this decorator.

Traps

Unsent requests

The HTTP client doesn't send the request when you call $http($request) to allow for concurrent calls. The call is actually done when you call ->match() on the returned Either.

This means that this code will not send the request:

$http = $os->remote()->http();
$http(Request::of(
    Url::of('https://your-service.com/api/some-resource'),
    Method::delete,
    ProtocolVersion::v11,
));

Even if you don't care about the response you need to do this:

$http = $os->remote()->http();
$http(Request::of(
    Url::of('https://your-service.com/api/some-resource'),
    Method::delete,
    ProtocolVersion::v11,
))->match(
    static fn() => null,
    static fn() => null,
);

Streaming

The default client uses cURL under the hood and the way it is structured prevents the streaming of requests/responses.

Info

However the work of the distributed abstraction will require the default client to switch to an implementation based on sockets that will open the door to streaming.