RRelayer
Home/Features

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 / methodContent
$res->statusHTTP status code (int)
$res->headersResponse headers array<string,string>
$res->bodyRaw 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, TraceableHttpClientCurlHttpClient; 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).

VariableContent
HTTP_CLIENT_TIMEOUTTimeout in seconds for the whole request
HTTP_CLIENT_CONNECT_TIMEOUTTimeout in seconds for establishing the connection

→ See also Environment Variables & .env Cascade.

Last updated: 2026-05-19
Change history (1)
  • Created