HTTP Client
Sometimes your app is the one making the request: calling a payment API, fetching a webhook target, talking to a sibling service. Fast ships a small outbound HTTP client for exactly that, reached through the http() helper. It speaks HTTP and HTTPS, and because it is built on the same non-blocking sockets as the server, a call made inside a request handler parks that one fiber while it waits on the network and lets the worker keep serving everyone else.
Making a request
http() returns a client that lives for the worker's lifetime. There is no use_*() setup to call first; the first request wires it up:
$res = http()->get('https://api.example.com/status');
if ($res->ok()) {
$data = $res->json();
}
The verb methods (get, post, put, patch, delete) all take a URL and an optional options array. For anything else, request($method, $url, $options) is the general form.
The response
Every call returns a Response value object:
$res->status(); // the status code, e.g. 200
$res->ok(); // true for any 2xx
$res->body(); // the raw response body as a string
$res->json(); // the body decoded as an array, or null if it is not JSON
$res->header('content-type'); // a single header, case-insensitive
$res->header('x-missing', 'default'); // with a fallback when absent
$res->headers(); // the whole lowercased header map
header() returns the first value of a header. That is worth knowing for Set-Cookie, which a server may send more than once; only the first reaches you here.
Sending a body
Pass json to send a JSON body (it sets Content-Type: application/json for you), form to send a URL-encoded form, or body for raw bytes you have already encoded:
http()->post('https://api.example.com/orders', [
'json' => ['sku' => 'A1', 'qty' => 2],
]);
http()->post('https://example.com/login', [
'form' => ['user' => 'ada', 'pass' => $secret],
]);
If you set your own Content-Type header, the client leaves it alone.
Options
| Option | Default | What it does |
|---|---|---|
headers | [] | Extra request headers. |
query | [] | Merged into the URL's query string (first request only). |
json | JSON body plus the JSON content type. | |
form | URL-encoded form body plus its content type. | |
body | '' | Raw body, used when json/form are absent. |
timeout | 30 | Total seconds for the whole call, redirects included. |
follow_redirects | true | Follow up to five hops; a number caps it, false turns it off. |
verify | true | Verify the TLS certificate. Leave this on. |
allow_private | false | Permit connections to private or reserved addresses. |
max_bytes | 8 MiB | Ceiling on the response body size. |
The query option is merged into the URL you pass, and only that first URL: a redirect carries its own query string and the client does not re-append yours on top.
Redirects
By default the client follows redirects, up to five hops, and does the sensible thing with the method. A 303 (and a 301/302 on a non-GET) becomes a GET with no body, matching how browsers behave; a 307/308 keeps the original method and body. If a redirect crosses to a different origin, the client drops your Authorization and Cookie headers so credentials meant for one host never leak to another.
Set follow_redirects to a number to cap the hops, or to false to get the redirect response back untouched and handle it yourself.
Timeouts
timeout is a single wall-clock budget for the entire call: DNS, connect, the TLS handshake, every read, and every redirect hop all draw from it. When it runs out the client throws, so a slow or hostile server cannot pin a worker open. Everything the client can go wrong on raises Fast\Http\Client\HttpClientException, so one catch covers timeouts, connection failures, TLS errors, and malformed responses.
use Fast\Http\Client\HttpClientException;
try {
$res = http()->get('https://api.example.com/slow', ['timeout' => 5]);
} catch (HttpClientException $e) {
// log and fall back
}
Reaching internal services (SSRF guard)
Because the client will happily fetch any URL, a URL that comes from user input is a classic server-side request forgery lever: someone hands you http://169.254.169.254/ or http://localhost/admin and your server fetches it from inside your network. To close that off, the client resolves the hostname itself and refuses to connect when the address lands in a loopback, private, or otherwise reserved range.
If you genuinely need to reach a local service, a dev proxy or a sidecar, opt in per request:
http()->get('http://127.0.0.1:9200/_health', ['allow_private' => true]);
What it does not do
The client is deliberately small, and a few things are out of scope by design:
- No connection pooling or keep-alive. Each request opens a fresh connection and
closes it. Simple and safe; not the tool for thousands of calls a second to one host.
- Synchronous DNS. Lookups block briefly, though results are cached for a short
window so only the first call to a host pays the cost.
- The whole body is buffered in memory, capped by
max_bytes. It is not a streaming
download client.
For most outbound calls, an API here, a webhook there, none of that matters. When it does, reach for a dedicated tool.