docs/contributing/exception-guidelines.md
Guzzle code must be deliberate about which exception it throws, because
GuzzleHttp\Client is a PSR-18 client: PSR-18 requires transfer failures
escaping sendRequest() to implement
Psr\Http\Client\ClientExceptionInterface. All Guzzle transfer exceptions
implement it via GuzzleHttp\Exception\GuzzleException, so "throw a Guzzle
exception" and "stay PSR-18 compliant" are the same rule. Exceptions thrown by
user callbacks that are documented to escape unwrapped, such as on_stats, are
not normalized into Guzzle exceptions.
This document covers the two exceptions contributors most often have to choose
between — InvalidArgumentException (caller error) and RequestException
(runtime request failure) — and the handful of cases where a bare SPL
exception is correct.
GuzzleHttp\Exception\NetworkException exists only in Guzzle 8.0 and newer. On
Guzzle 7.x maintenance branches, network failures without a response use
ConnectException, which implements Psr\Http\Client\NetworkExceptionInterface
directly.
Is the failure caused by the CALLER passing an invalid argument / option / value
that they could fix by changing their code?
├─ YES ─────────────────────────────► GuzzleHttp\Exception\InvalidArgumentException
│ (a ClientExceptionInterface, but NOT a
│ RequestExceptionInterface — see below)
│
└─ NO — it is a runtime failure.
│
├─ Is it happening while sending a specific request (RequestInterface in scope)?
│ ├─ Network failure, no response yet ──► NetworkException / ConnectException
│ │ / ConnectTimeoutException / NetworkTimeoutException
│ ├─ A response was received, then it failed
│ │ ├─ transfer failure ──► ResponseTransferException / ResponseTimeoutException
│ │ ├─ local/callback/finalization failure ──► ResponseException
│ │ └─ bad response policy failure ──► BadResponseException
│ └─ Cannot even begin sending this request
│ (curl handle init, sink directory missing, unsupported protocol) ──► RequestException
│
└─ No request in scope — an environment / construction-time check
(e.g. "the cURL extension is not available") ──► \RuntimeException (bare SPL is fine here)
Use ResponseTransferException for transfer-level failures after response
headers were received and a response object exists. This includes response-aware
connection, network, protocol, content-decoding, partial-body, and response-body
transfer failures.
Do not use ResponseTransferException for local sink writes, request-body
stream failures, progress callback failures, on_headers failures, or response
finalization such as rewinding a response sink after the body has been received.
Deterministic platform-limit failures, such as a response length or byte count
that cannot be represented as a PHP integer, are also plain ResponseException
failures when a response exists.
Timeouts: transport vs. caller-supplied streams. A *TimeoutException means
the network transport timed out, such as cURL CURLE_OPERATION_TIMEDOUT or a
stream send/connect timeout message. A timeout surfaced as a PSR-7
TimeoutException from a caller stream is classified like any other failure of
that stream: RequestException while reading the request body before a
response, or ResponseException once a response exists. It is never a
NetworkTimeoutException.
Note: ResponseException extends RequestException, so it (and every subtype,
including ResponseTransferException and ResponseTimeoutException) is a
PSR-18 RequestExceptionInterface. PSR-18 defines no interface for "a response
was received, then the transfer failed": its NetworkExceptionInterface is a
no-response interface, so among PSR-18's request-bound interfaces only
RequestExceptionInterface remains once Guzzle holds a response. This is a
deliberate classification, not a claim that the request message was malformed.
GuzzleHttp\Exception\InvalidArgumentExceptionfinal class InvalidArgumentException extends \InvalidArgumentException implements GuzzleException
Use it when the caller supplied something invalid: a bad option value, the wrong type, conflicting options, a non-callable callback, or a path that does not exist. It is the only SPL-extending Guzzle exception, by design.
Why this type and not RequestException? Invalid options are not an
invalid request. The request message is well-formed; the caller misconfigured
the client. PSR-18 reserves RequestExceptionInterface for malformed request
messages ("method is missing", "body not seekable"). So this class implements
GuzzleException (→ ClientExceptionInterface) but deliberately does not
implement RequestExceptionInterface, and it does not carry a request.
(Decision recorded in guzzle/guzzle#2474.)
use GuzzleHttp\Exception\InvalidArgumentException;
// GOOD — caller passed a non-callable for a callback option
if (!\is_callable($options['on_headers'])) {
throw new InvalidArgumentException('on_headers must be callable');
}
// GOOD — caller pointed an option at a file that does not exist
if (!\file_exists($options['verify'])) {
throw new InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
}
Do NOT:
// BAD — the global SPL type does NOT implement ClientExceptionInterface, so it
// escapes sendRequest() and violates PSR-18. Never use it on the request path.
throw new \InvalidArgumentException('on_headers must be callable');
// BAD — invalid options are not a malformed request; do not make them a
// RequestExceptionInterface or attach a request to them.
throw new RequestException('on_headers must be callable', $request);
Because
GuzzleHttp\Exception\InvalidArgumentException extends \InvalidArgumentException,
this is backward compatible: existing catch (\InvalidArgumentException) blocks
still catch it. catch blocks that need both forms must use the global type:
} catch (\InvalidArgumentException $e) { // catches global AND Guzzle's
Middleware sometimes calls PSR-17 factories or PSR-7 parsers while handling a
response — for example resolving a redirect Location header through the
configured uri_factory. Those components are allowed to throw the global
\InvalidArgumentException, so the wrapping catch must use the global type,
and the failure should surface as the appropriate response-aware exception
(BadResponseException for a malformed Location, since a response exists):
try {
$uri = $uriFactory->createUri($location);
} catch (\InvalidArgumentException $e) { // global type: also catches Guzzle's
throw new BadResponseException("Redirect URI, {$location}, is invalid", $request, $response, $e);
}
Keep Guzzle-owned option validation outside such try blocks. Because
GuzzleHttp\Exception\InvalidArgumentException extends the global class, a
broad catch (\InvalidArgumentException) would otherwise reclassify a caller
error (the wrong uri_factory type, an invalid allow_redirects value) as a
response failure. Validate the option first and throw the Guzzle type, then wrap
only the third-party call:
// GOOD — caller error is thrown before the wrapping try block.
if (!$uriFactory instanceof UriFactoryInterface) {
throw new InvalidArgumentException('uri_factory must be an instance of '.UriFactoryInterface::class);
}
GuzzleHttp\Exception\RequestExceptionclass RequestException extends TransferException implements RequestExceptionInterface
(and TransferException extends \RuntimeException implements GuzzleException).
Use it when a runtime failure prevents Guzzle from completing a specific
request, you have the RequestInterface in scope, and no more specific type
applies (it is not a network failure, and no response was received). Typical
cases: the cURL easy handle could not be initialised, the sink directory does
not exist, or the requested protocol/scheme is not supported by the handler.
use GuzzleHttp\Exception\RequestException;
// GOOD — runtime failure setting up THIS request; carries the request,
// is a ClientExceptionInterface AND a RequestExceptionInterface.
if (false === $handle) {
throw new RequestException('Can not initialize cURL handle.', $request);
}
Do NOT:
// BAD — bare \RuntimeException on the request path is not a ClientExceptionInterface
// (PSR-18 leak), and drops the request.
throw new \RuntimeException('Can not initialize cURL handle.');
// BAD — RequestException is for failures BEFORE a response. If a response was
// received, use the ResponseException family instead.
throw new RequestException('Body transfer failed', $request); // use ResponseTransferException
There is intentionally no GuzzleHttp\Exception\RuntimeException
(guzzle/guzzle#2507): request-bound runtime failures use
RequestException/TransferException; only non-request environment checks use
the bare \RuntimeException.
// OK — construction/environment check, no request involved, not on the
// per-request sendRequest() path.
if (!\extension_loaded('curl')) {
throw new \RuntimeException('The cURL extension is required.');
}
These are deliberate programmer-error signals. They are not
GuzzleExceptions, and that is intentional — they indicate misuse of a
Guzzle-specific API, not a failure to transfer an HTTP request, and they are
unreachable through a normal PSR-18 sendRequest() call:
| Situation | Exception | Why bare SPL |
|---|---|---|
Reusing a handler/factory after close() | \BadMethodCallException | Lifecycle misuse (a LogicException); reachable only via the 8.0 close() API |
| Accessing an unsupported magic property | \BadMethodCallException | Programmer error |
MockHandler queue exhausted | \OutOfBoundsException | Test-setup error |
MockHandler queued an unsupported value | \TypeError | Programming error (an \Error) |
Do not "wrap" these into GuzzleExceptions to satisfy PSR-18 strictly: it would
break catch (\BadMethodCallException) / catch (\OutOfBoundsException) and
the tests that assert them, and PSR-18 governs sendRequest() transfers, not
misuse of Guzzle's own lifecycle APIs.
\InvalidArgumentException from code reachable
while preparing or sending a request — use
GuzzleHttp\Exception\InvalidArgumentException.GuzzleHttp\Exception\RuntimeException — it does not
exist. Use RequestException/TransferException (request-bound) or bare
\RuntimeException (environment-only).InvalidArgumentException a RequestExceptionInterface or
give it a request — invalid options are not a malformed request.sendRequest() — that is http_errors
middleware, which PSR-18 mode disables.Network*/Connect* (no response), ResponseTransferException
(response-aware transfer failure), plain ResponseException (other
response-aware failure), or RequestException (can't start) by transfer
phase; see the hierarchy in UPGRADING.md.catch (\GuzzleHttp\Exception\GuzzleException) (or PSR-18's
Psr\Http\Client\ClientExceptionInterface) catches every Guzzle transfer error.
InvalidArgumentException is also catchable as \InvalidArgumentException. The
bare-SPL signals above (closed handler, exhausted mock) are programmer errors
you should fix, not catch.