diff --git a/README.md b/README.md index dab7ca2..b8c76a4 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ Generic `Request` ensures `$client->send(request: $request)` returns composer require zlodes/http-client ``` -You'll also need a PSR-7 implementation and a PSR-18 HTTP client: +You'll also need a PSR-7 implementation and any PSR-18 HTTP client, for example: ```bash -composer require guzzlehttp/psr7 guzzlehttp/guzzle +composer require guzzlehttp/psr7 symfony/http-client ``` Optional integrations such as logging and metrics are up to your application and middleware choices. @@ -378,10 +378,7 @@ $client = new HttpClient( ); ``` -Which transport actually applies the options: - -- `GuzzleTransport` maps them to Guzzle's `timeout` and `connect_timeout`, and sends with `http_errors` disabled. A cURL timeout (error 28) is thrown as `TransportTimeoutException`, which extends `TransportException`, so a timeout is distinguishable from a refused connection. -- `Psr18Transport` ignores them. PSR-18 has no per-request options, so configure timeouts on the wrapped client instead. +The bundled `Psr18Transport` ignores them. PSR-18 has no per-request options, so configure timeouts on the wrapped client instead. To honour per-request options, implement your own `Transport` and read them from the `TransportOptions` argument. Throw `TransportTimeoutException` (extends `TransportException`) on timeout so callers can tell a timeout from a refused connection. ## Default headers diff --git a/UPGRADE-1.0.md b/UPGRADE-1.0.md index 524c7b2..5c99db0 100644 --- a/UPGRADE-1.0.md +++ b/UPGRADE-1.0.md @@ -51,6 +51,4 @@ $transport->shouldReceive('send')->once()->with(Mockery::on($matcher), Mockery:: Set them with `WithTimeout` on a `ClientFactory`, with `TimeoutMiddleware` directly, or per endpoint by implementing `HasTransportOptions`. Precedence is request > client > global default. -`Psr18Transport` ignores `TransportOptions`. PSR-18 has no per-request options; configure timeouts on the wrapped client. - -`GuzzleTransport` applies them. It requires `guzzlehttp/guzzle`, which this library now depends on. +`Psr18Transport` ignores `TransportOptions`. PSR-18 has no per-request options; configure timeouts on the wrapped client, or implement a custom `Transport` that applies them. diff --git a/composer.json b/composer.json index d4844b6..22578e5 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,6 @@ "type": "library", "require": { "php": "^8.4", - "guzzlehttp/guzzle": "^7.9", "psr/http-client": "^1.0", "psr/http-message": "^2.0" }, diff --git a/src/Transport/GuzzleTransport.php b/src/Transport/GuzzleTransport.php deleted file mode 100644 index 3e6d9dc..0000000 --- a/src/Transport/GuzzleTransport.php +++ /dev/null @@ -1,61 +0,0 @@ - false]; - - if ($options->timeout !== null) { - $guzzleOptions[RequestOptions::TIMEOUT] = $options->timeout; - } - - if ($options->connectTimeout !== null) { - $guzzleOptions[RequestOptions::CONNECT_TIMEOUT] = $options->connectTimeout; - } - - try { - return $this->client->send($request, $guzzleOptions); - } catch (ConnectException $e) { - if ($this->isTimeout($e)) { - throw new TransportTimeoutException($e->getMessage(), (int) $e->getCode(), $e); - } - - throw new TransportException($e->getMessage(), (int) $e->getCode(), $e); - } catch (GuzzleException $e) { - throw new TransportException($e->getMessage(), (int) $e->getCode(), $e); - } - } - - private function isTimeout(ConnectException $exception): bool - { - $context = $exception->getHandlerContext(); - - return ($context['errno'] ?? null) === self::CURL_OPERATION_TIMEDOUT; - } -} diff --git a/tests/Transport/GuzzleTransportTest.php b/tests/Transport/GuzzleTransportTest.php deleted file mode 100644 index 26d39cf..0000000 --- a/tests/Transport/GuzzleTransportTest.php +++ /dev/null @@ -1,204 +0,0 @@ -captured = $options; - - return $this->response; - } - - public function sendAsync(RequestInterface $request, array $options = []): \GuzzleHttp\Promise\PromiseInterface - { - throw new \LogicException('not used'); - } - - public function request(string $method, $uri, array $options = []): ResponseInterface - { - throw new \LogicException('not used'); - } - - public function requestAsync(string $method, $uri, array $options = []): \GuzzleHttp\Promise\PromiseInterface - { - throw new \LogicException('not used'); - } - - public function getConfig(?string $option = null): mixed - { - throw new \LogicException('not used'); - } - }; - - $response = (new GuzzleTransport($client))->send( - $request, - new TransportOptions(timeout: 5, connectTimeout: 1.5), - ); - - self::assertSame($expected, $response); - self::assertSame([ - 'http_errors' => false, - RequestOptions::TIMEOUT => 5, - RequestOptions::CONNECT_TIMEOUT => 1.5, - ], $captured); - } - - public function testOmitsUnsetOptions(): void - { - $captured = null; - - $client = new class ($captured) implements ClientInterface { - public function __construct(private ?array &$captured) - { - } - - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - $this->captured = $options; - - return new Response(200); - } - - public function sendAsync(RequestInterface $request, array $options = []): \GuzzleHttp\Promise\PromiseInterface - { - throw new \LogicException('not used'); - } - - public function request(string $method, $uri, array $options = []): ResponseInterface - { - throw new \LogicException('not used'); - } - - public function requestAsync(string $method, $uri, array $options = []): \GuzzleHttp\Promise\PromiseInterface - { - throw new \LogicException('not used'); - } - - public function getConfig(?string $option = null): mixed - { - throw new \LogicException('not used'); - } - }; - - (new GuzzleTransport($client))->send(new Request('GET', 'https://example.com'), TransportOptions::none()); - - self::assertSame(['http_errors' => false], $captured); - } - - public function testConnectTimeoutBecomesTransportTimeoutException(): void - { - $request = new Request('GET', 'https://example.com'); - $connectException = new ConnectException( - 'cURL error 28: Connection timed out', - $request, - null, - ['errno' => 28], - ); - - $client = $this->clientThrowing($connectException); - - try { - (new GuzzleTransport($client))->send($request, new TransportOptions(timeout: 5)); - self::fail('Expected TransportTimeoutException'); - } catch (TransportTimeoutException $e) { - self::assertSame($connectException, $e->getPrevious()); - } - } - - public function testConnectionRefusedStaysTransportException(): void - { - $request = new Request('GET', 'https://example.com'); - $connectException = new ConnectException( - 'cURL error 7: Connection refused', - $request, - null, - ['errno' => 7], - ); - - $client = $this->clientThrowing($connectException); - - try { - (new GuzzleTransport($client))->send($request, TransportOptions::none()); - self::fail('Expected TransportException'); - } catch (TransportTimeoutException) { - self::fail('A refused connection must not be reported as a timeout'); - } catch (TransportException $e) { - self::assertSame($connectException, $e->getPrevious()); - } - } - - public function testOtherGuzzleExceptionStaysTransportException(): void - { - $request = new Request('GET', 'https://example.com'); - $requestException = new RequestException('bad request', $request); - - $this->expectException(TransportException::class); - - (new GuzzleTransport($this->clientThrowing($requestException)))->send($request, TransportOptions::none()); - } - - private function clientThrowing(\Throwable $exception): ClientInterface - { - return new class ($exception) implements ClientInterface { - public function __construct(private readonly \Throwable $exception) - { - } - - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw $this->exception; - } - - public function sendAsync(RequestInterface $request, array $options = []): \GuzzleHttp\Promise\PromiseInterface - { - throw new \LogicException('not used'); - } - - public function request(string $method, $uri, array $options = []): ResponseInterface - { - throw new \LogicException('not used'); - } - - public function requestAsync(string $method, $uri, array $options = []): \GuzzleHttp\Promise\PromiseInterface - { - throw new \LogicException('not used'); - } - - public function getConfig(?string $option = null): mixed - { - throw new \LogicException('not used'); - } - }; - } -}