SUQODocs
User GuideAPI ReferenceSDKs

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:

ExceptionRaise it whenBecomes
Http\HttpCancelledExceptionthe token was cancelledCancelledError
Http\HttpClientExceptionany other transport failure, timeout includedNetworkError

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->timeout is 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.

On this page