HTTP client
Quiote ships a PSR-18 HTTP client for outbound requests. It’s a named-client factory (modelled on .NET’s IHttpClientFactory): you configure a client once by name, resolve it by name, and the same instance is reused for the worker’s lifetime rather than rebuilt per call. It’s also the central egress seam that makes outbound trace propagation and CLIENT spans work (see Telemetry).
Nothing here is on by default — the client is opt-in, and its default transport is a zero-dependency curl PSR-18 client. If Guzzle (which is a PSR-18 client) is installed, it’s used automatically; it’s never required.
Configuring a named client
Section titled “Configuring a named client”Clients are configured in code rather than in a Config/* file. The usual home for that call is a plugin’s register() method — the registrar exposes httpClient() for exactly this (shown in Resolving the factory below) — so every client is defined once at boot and resolved by name anywhere afterwards.
Register a named client’s configuration on the HttpClientFactory, then resolve it:
use Quiote\Http\Client\{HttpClientFactory, HttpClientConfig};
$factory->configure('github', function (HttpClientConfig $c): void { $c->baseUri('https://api.github.com') ->header('Accept', 'application/vnd.github+json') ->retry(attempts: 3, baseDelayMs: 100);});
$response = $factory->client('github')->get('/repos/quioteframework/quiote');client($name) builds the client on first use and memoizes it; client() with no name resolves the default client. The configurator runs lazily, once.
HttpClientConfig methods:
| Method | Effect |
|---|---|
baseUri(string) | Prefix for relative request URIs. |
header(string $name, string $value) | A default header sent on every request. |
headers(array) | Multiple default headers at once. |
retry(int $attempts, int $baseDelayMs = 100) | Retry policy — retries transient failures (network errors, 429, 5xx) with backoff. |
transport(ClientInterface) | Use a specific PSR-18 client instead of the auto-selected default. |
Making requests
Section titled “Making requests”HttpClient implements PSR-18 ClientInterface (sendRequest()), plus convenience methods:
$client = $factory->client('github');
$client->get('/user');$client->post('/repos/acme/app/issues', [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode(['title' => 'Bug']),]);$client->put('/gists/123', $options);$client->delete('/gists/123');$client->request('PATCH', '/issues/1', $options);The options array these convenience methods accept has exactly two keys — headers (a name => value map) and body (a raw string). There is no json option: encode the payload yourself with json_encode() and set Content-Type in headers, as above. $options in the put/request calls is just such an array.
Resolving the factory
Section titled “Resolving the factory”HttpClientFactory is registered as a container singleton (alias http_client_factory), so inject it where you need it:
use Quiote\Http\Client\HttpClientFactory;
final class GitHubService{ public function __construct(private HttpClientFactory $http) {}
public function repo(string $full): array { $res = $this->http->client('github')->get("/repos/$full"); return json_decode((string) $res->getBody(), true); }}See The DI container for injection. From a plugin, contribute a named client with $registrar->httpClient('github', fn($c) => ...).
Transports
Section titled “Transports”A “transport” is any PSR-18 ClientInterface. Two are available out of the box:
CurlTransport— the zero-dependency default; builds PSR-7 responses via Nyholm and maps curl connection/timeout failures to the PSR-18 network/request exceptions.- Guzzle — used automatically if
guzzlehttp/guzzleis installed (it already implements PSR-18); no adapter needed.
TransportFactory::default() returns Guzzle if present, else curl. Override per client with HttpClientConfig::transport(), or globally with HttpClientFactory::setDefaultTransportFactory().
Trace propagation
Section titled “Trace propagation”When telemetry is enabled, every request through HttpClient automatically:
- opens a
SpanKind::Clientspan (categoryQuiote.Http.Client, name"HTTP {method}"), - injects the W3C
traceparentheader into the outbound request, so the downstream service continues the same trace, - records
http.request.method,url.full, andhttp.response.status_code, and captures exceptions.
All of it is gated on Trace::enabled() and guarded — telemetry never changes whether a request succeeds. This is the outbound half of context propagation: requests made through this client are traced end to end; requests made with a raw curl/socket bypass it.