Inject an HTTP client
Swapping the cURL client for PSR-18, and the guarantees you take on when you do.
The default is cURL. Swap it at construction:
use Suqo\Http\CurlHttpClient;
use Suqo\Http\Psr18HttpClient;
use Suqo\SuqoClient;
// cURL, with your own options — a proxy, a CA bundle, an interface binding.
$suqo = new SuqoClient(httpClient: new CurlHttpClient([CURLOPT_PROXY => '…']));
// Or any PSR-18 client.
$suqo = new SuqoClient(httpClient: new Psr18HttpClient($client, $requestFactory, $streamFactory));Psr18HttpClient requires psr/http-client and psr/http-factory, which are suggested rather than required dependencies.
Every request carries your API key
Authorization: Bearer <key> is set on every request the SDK makes. That one fact drives everything below: anything that changes where a request goes, or who can read it, is a question about your live credential.
Redirects are never followed — do not re-enable them
CurlHttpClient forces CURLOPT_FOLLOWLOCATION off, and does so after merging your options, so passing it as true has no effect. cURL does not strip a manually-set Authorization header when it follows a redirect, so a 302 to another host would hand your live key to that host.
A redirect therefore reaches the transport intact and is raised as a SuqoError carrying the 3xx status, rather than being decoded as if its body were the payload. It is not retried: a redirect is a contract change, not a transient failure.
If you inject a PSR-18 client, this guarantee is yours to keep — the adapter copies headers onto a PSR-7 request and delegates; it cannot control your client's redirect policy, and several popular clients follow redirects by default:
// Guzzle
new \GuzzleHttp\Client(['allow_redirects' => false]);
// Symfony HttpClient
\Symfony\Component\HttpClient\HttpClient::create(['max_redirects' => 0]);With redirects disabled you will see a SuqoError with a 3xx status if the API ever redirects — which is the outcome you want. Leave them enabled and the key has already been sent by the time the SDK sees anything.
Do not disable TLS verification
CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are not among the options the SDK overrides, so a value you pass is honoured. That is a deliberate escape hatch — the same mechanism that lets you set a proxy or a custom CA bundle — but it means the SDK will happily send your API key over a connection it has not authenticated. If you need a custom trust root, point at it rather than switching verification off:
new CurlHttpClient([CURLOPT_CAINFO => '/path/to/ca-bundle.crt']);Your options are a base, not an override
CurlHttpClient applies your array first, then writes the options it requires on top: URL, method, headers, timeouts, body, and the redirect and progress settings. You can add to the request; you cannot change what the SDK depends on.
Writing your own client
The interface is one method:
public function send(HttpRequest $request): HttpResponse;Return an HttpResponse for any HTTP status, 5xx included — do not throw on a status, the transport maps statuses. Throw exactly two types, and wrap your client's own exceptions in them:
| Exception | Raise it when | Becomes |
|---|---|---|
Http\HttpCancelledException | the token was cancelled | CancelledError |
Http\HttpClientException | any other transport failure, timeout included | NetworkError |
HttpCancelledException extends HttpClientException, so catch the cancelled one first. Anything else escaping send() propagates raw and unmapped.
use Suqo\Http\HttpClientInterface;
use Suqo\Http\HttpRequest;
use Suqo\Http\HttpResponse;
final class LoggingHttpClient implements HttpClientInterface
{
public function __construct(private readonly HttpClientInterface $inner)
{
}
public function send(HttpRequest $request): HttpResponse
{
error_log("{$request->method} {$request->url}");
return $this->inner->send($request);
}
}
$suqo = new SuqoClient(httpClient: new LoggingHttpClient(new CurlHttpClient()));HttpRequest carries method, url (absolute, query string included), headers, body (already-serialised JSON or null), timeout and cancellation. HttpResponse takes status, headers and the raw body; header names are lower-cased on construction, and header() looks them up case-insensitively.
Two PSR-18 caveats
Both are inherent to the standard rather than to this adapter:
- No per-request timeout. PSR-18 cannot express one, so
$request->timeoutis ignored — configure the timeout on your own client. - Cancellation only at boundaries. The token is checked before the request and after it returns, never mid-flight.