> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instructorphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 9 1 custom clients

The bundled drivers cover the most common HTTP libraries, but there are situations where you need a custom integration -- perhaps with a proprietary HTTP library, a legacy system, or a specialized transport. This chapter shows how to create a custom driver, register it with the driver registry, and use it through the standard client API.

## The Driver Contract

Every driver must implement the `CanHandleHttpRequest` interface, which defines a single method:

```php theme={null}
namespace Cognesy\Http\Contracts;

interface CanHandleHttpRequest
{
    public function handle(HttpRequest $request): HttpResponse;
}
// @doctest id="0e49"
```

The method receives an `HttpRequest` and returns an `HttpResponse`. That is the entire contract. The driver is responsible for converting these value objects into whatever the underlying HTTP library expects.

## Creating a Custom Driver

Here is a template for a custom driver:

```php theme={null}
namespace App\Http\Drivers;

use Cognesy\Http\Config\HttpClientConfig;
use Cognesy\Http\Contracts\CanHandleHttpRequest;
use Cognesy\Http\Data\HttpRequest;
use Cognesy\Http\Data\HttpResponse;
use Cognesy\Http\Exceptions\HttpRequestException;
use Cognesy\Events\Contracts\CanHandleEvents;

class AcmeHttpDriver implements CanHandleHttpRequest
{
    public function __construct(
        private HttpClientConfig $config,
        private CanHandleEvents $events,
        private ?object $clientInstance = null,
    ) {
        // Initialize your vendor client here
        $this->client = $clientInstance ?? new \Acme\HttpClient([
            'connect_timeout' => $config->connectTimeout,
            'timeout' => $config->requestTimeout,
        ]);
    }

    public function handle(HttpRequest $request): HttpResponse
    {
        try {
            $vendorResponse = $this->client->request(
                method: $request->method(),
                url: $request->url(),
                headers: $request->headers(),
                body: $request->body()->toString(),
            );

            if ($request->isStreamed()) {
                return HttpResponse::streaming(
                    statusCode: $vendorResponse->status(),
                    headers: $vendorResponse->headers(),
                    stream: $this->adaptStream($vendorResponse),
                );
            }

            return HttpResponse::sync(
                statusCode: $vendorResponse->status(),
                headers: $vendorResponse->headers(),
                body: $vendorResponse->body(),
            );
        } catch (\Exception $e) {
            throw new HttpRequestException(
                message: $e->getMessage(),
                request: $request,
                previous: $e,
            );
        }
    }

    private function adaptStream($response): \Cognesy\Http\Stream\StreamInterface
    {
        return \Cognesy\Http\Stream\BufferedStream::fromStream(
            (function () use ($response) {
                foreach ($response->getStream() as $chunk) {
                    yield $chunk;
                }
            })()
        );
    }
}
// @doctest id="38ea"
```

The key points are:

* Accept `HttpClientConfig`, `CanHandleEvents`, and an optional vendor client instance in the constructor. This matches the signature expected by the driver registry.
* Return `HttpResponse::sync()` for buffered responses and `HttpResponse::streaming()` for streamed responses.
* Wrap vendor exceptions in `HttpRequestException` to maintain a consistent exception hierarchy.

## Registering the Driver

To make your driver available by name (e.g., `'acme'`), register it with the driver registry:

```php theme={null}
use Cognesy\Http\Config\HttpClientConfig;
use Cognesy\Http\Contracts\CanHandleHttpRequest;
use Cognesy\Http\Creation\BundledHttpDrivers;
use Cognesy\Events\Contracts\CanHandleEvents;

$drivers = BundledHttpDrivers::registry()->withDriver(
    'acme',
    static fn(HttpClientConfig $config, CanHandleEvents $events, ?object $clientInstance): CanHandleHttpRequest
        => new AcmeHttpDriver($config, $events, $clientInstance),
);
// @doctest id="05ff"
```

Then use it through the builder:

```php theme={null}
use Cognesy\Http\Config\HttpClientConfig;
use Cognesy\Http\Creation\HttpClientBuilder;

$client = (new HttpClientBuilder())
    ->withDrivers($drivers)
    ->withConfig(new HttpClientConfig(driver: 'acme'))
    ->create();
// @doctest id="8a8e"
```

The factory function receives the config, events dispatcher, and optional client instance. This lets users pass a pre-configured vendor client through `withClientInstance('acme', $myClient)`.

## Injecting a Driver Directly

If you do not need the registry, bypass it entirely by passing a driver instance:

```php theme={null}
use Cognesy\Http\Creation\HttpClientBuilder;

$driver = new AcmeHttpDriver($config, $events);

$client = (new HttpClientBuilder())
    ->withDriver($driver)
    ->create();
// @doctest id="2181"
```

Or use the static shorthand:

```php theme={null}
$client = HttpClient::fromDriver($driver);
// @doctest id="a746"
```

## Reusing Vendor Client Instances

When your vendor client requires special setup (custom SSL certificates, proxy configuration, connection pools), create the instance yourself and pass it through:

```php theme={null}
use Cognesy\Http\Creation\HttpClientBuilder;
use Symfony\Component\HttpClient\HttpClient as SymfonyHttpClient;

$symfony = SymfonyHttpClient::create([
    'proxy' => 'http://proxy.internal:8080',
    'verify_peer' => true,
    'cafile' => '/etc/ssl/custom-ca.pem',
]);

$client = (new HttpClientBuilder())
    ->withClientInstance('symfony', $symfony)
    ->create();
// @doctest id="5268"
```

This pattern works with any registered driver. The `withClientInstance()` method sets both the driver name and the instance, so the driver factory receives it instead of creating its own.

## Streaming in Custom Drivers

The `HttpResponse::streaming()` factory accepts a `StreamInterface` implementation. The simplest approach is to yield chunks from a generator and wrap them with `BufferedStream::fromStream()`:

```php theme={null}
public function handle(HttpRequest $request): HttpResponse
{
    $vendorResponse = $this->client->sendStreaming($request->url(), ...);

    $stream = (function () use ($vendorResponse) {
        foreach ($vendorResponse->chunks() as $chunk) {
            yield $chunk;
        }
    })();

    return HttpResponse::streaming(
        statusCode: $vendorResponse->statusCode(),
        headers: $vendorResponse->headers(),
        stream: BufferedStream::fromStream($stream),
    );
}
// @doctest id="29a3"
```

The `BufferedStream`, `ArrayStream`, `IterableStream`, and `TransformStream` classes in the `Cognesy\Http\Stream` namespace provide various stream implementations you can use or compose.

## See Also

* [Changing Client](7-changing-client) -- switch between drivers without custom code.
* [Changing Client Config](8-changing-client-config) -- configure timeouts and error handling.
* [Middleware](10-middleware) -- add behaviors around any driver.
