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)
);
- 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:
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.