HTTP Client#
A thin contract for hitting external web APIs. It is the "external API version" of Database: a page/component takes an HttpClient dependency and calls it directly.
It requires no configuration and is always registered in DI (auto-registered at the registerDefaults stage, the same as EtagStore). It deliberately has no client builder / middleware stack / PSR-18 (the policy is to keep it thin — the same stance as Database not having a query builder). ext-curl is required (the base of the production image already includes it).
Usage#
Just take an HttpClient by type as an argument.
<?php
use Polidog\Relayer\Http\Client\HttpClient;
use Polidog\Relayer\Router\Component\PageContext;
return function (PageContext $ctx, HttpClient $http): Closure {
$res = $http->get('https://api.example.com/users/1');
if (!$res->ok()) {
$ctx->abort($res->status >= 500 ? 502 : 404);
}
$user = $res->json();
return fn () => <h1>{$user['name']}</h1>;
};get($url, $headers = [])— a shortcut for GET.request($method, $url, $headers = [], $body = null)— the general
form.
Headers are passed as name => value, like ['Authorization' => 'Bearer …'].
HttpResponse#
The return value is a readonly HttpResponse (its constructor is open because it is a plain carrier, not an output contract).
| Member / method | Content |
|---|---|
$res->status | HTTP status code (int) |
$res->headers | Response headers array<string,string> |
$res->body | Raw body ('' when absent) |
$res->ok() | true if 2xx |
$res->json() | JSON-decodes the body (associative array). Invalid JSON throws HttpClientException |
$res->header($name) | Gets a single header case-insensitively (null if absent) |
This is distinct from the Response that the server returns to the browser (this one is what an external API returned to us).
Handling errors#
4xx/5xx are not errors. They come back as a normal HttpResponse that always has status/headers/body filled in, so you branch on $res->status / $res->ok() (the same as a 0-row SELECT not being an exception).
An exception is thrown only on a transport failure (DNS / connection / TLS / timeout / no body arrives). There is just one type, HttpClientException, and the original driver exception is kept in previous. Passing non-JSON to $res->json() is the same exception.
use Polidog\Relayer\Http\Client\HttpClientException;
try {
$res = $http->get($url);
} catch (HttpClientException $e) {
// DNS/connection/TLS/timeout etc. There is no response
}Request-scoped memoization#
The concrete HttpClient is CachingHttpClient (in dev, TraceableHttpClient → CurlHttpClient; in production, CurlHttpClient directly). Only safe methods (GET/HEAD) are memoized within a single request. Other methods are sent every time, and they also drop the request-scoped cache, so "read → write → read again" lets you see your own write. In dev the actual round trips line up in the Profiler timeline.
Timeouts#
The CurlHttpClient timeouts can be tuned with environment variables (optional, in seconds; if unset, the cURL default applies).
| Variable | Content |
|---|---|
HTTP_CLIENT_TIMEOUT | Timeout in seconds for the whole request |
HTTP_CLIENT_CONNECT_TIMEOUT | Timeout in seconds for establishing the connection |
→ See also Environment Variables & .env Cascade.
Change history (1)
- Created