Repository navigation
Support Guzzle 8 alongside Guzzle 7 - #13
Merged
Merged
Conversation
Guzzle 8 removes RetryMiddleware::exponentialDelay() and reclassifies network failures: a timeout before the response headers is NetworkTimeoutException, no longer a ConnectException, and send or receive errors become NetworkException instead of RequestException. Pin today's behaviour on Guzzle 7 first, so the Guzzle 8 support can show it keeps it: - a timeout without a response is retried; - a receive error after the request was sent is not, because the provider may already have run (and billed) it; - without Retry-After, the delays are 1 s, 2 s, 4 s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Applications on Guzzle 8 cannot install this library: it pins guzzlehttp/guzzle ^7.9, promises ^2.0 and psr7 ^2.7. Allowing the new majors alone is not enough, because two things break on Guzzle 8: - RetryMiddleware::exponentialDelay() is gone (deprecated since 7.11), so every retry without a Retry-After header died with "Call to undefined method". The delay is now computed here, with the same formula (1 s, 2 s, 4 s). - A timeout before the response headers is NetworkTimeoutException, no longer a ConnectException, so it was not retried any more. The decider now also retries that class. It keeps everything else as on Guzzle 7: Guzzle 8's other NetworkExceptions (send and receive errors) are not retried, because the request may already have been processed. The only narrowing is the empty reply (cURL 52), which Guzzle 7 reported as ConnectException and Guzzle 8 does not. Also: - PHPStan's generics.notGeneric ignore only matches with Promises 2, so it no longer reports when it is unmatched. - A CI job runs the tests and PHPStan on the newest Guzzle 7, now that a plain install picks Guzzle 8. The prefer-lowest job keeps covering the oldest Guzzle 7. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Applications on Guzzle 8 can't install
soukicz/llmtoday, because it pinsguzzlehttp/guzzle ^7.9,guzzlehttp/promises ^2.0andguzzlehttp/psr7 ^2.7. That blocks Guzzle 8 in microservice/invoice-extractor, for example. This PR allows Guzzle 8 (with Promises 3 and PSR-7 3) and keeps Guzzle 7.9+ working.Widening the constraints alone is not enough. On Guzzle 8 the unchanged code breaks in two places:
Retry-Afterheader crashes.RetryMiddleware::exponentialDelay()was removed in 8 (it has been deprecated since 7.11), so the call fails withError: Call to undefined method.NetworkTimeoutException, which is no longer aConnectException, so the retry decider gave up after one attempt. A real cURL run against a slow local server showed it: 1 attempt on Guzzle 8, against 4 on Guzzle 7.Changes
test: pin retry delays and which network failures are retried: tests written onmasterfirst; they pass there on Guzzle 7.RequestException, Guzzle 8 asNetworkException.Retry-After, the delays are 1 s, 2 s, 4 s. A middleware pushed below the retry middleware recordsdelayand drops it, so the tests don't sleep.feat: support Guzzle 8 alongside Guzzle 7:composer.json:guzzle ^7.9 || ^8.0,promises ^2.0 || ^3.0,psr7 ^2.7 || ^3.0.HttpClientFactory:ConnectException, plus Guzzle 8'sNetworkTimeoutException, referenced by class name so the code still loads and passes PHPStan on Guzzle 7.phpstan.neon: thegenerics.notGenericignore matches only with Promises 2, so it is markedreportUnmatched: false.Retry policy: kept as on Guzzle 7, on purpose
Guzzle 8 reclassifies more no-response failures as
NetworkException, such as send and receive errors after the request went out. CatchingPsr\Http\Client\NetworkExceptionInterfacewould have retried those POSTs, and the provider may already have run, and billed, them. So the decider keeps Guzzle 7's set: failed connections, where nothing was sent, and timeouts before a response.The one narrowing: an empty reply (cURL 52) was a
ConnectExceptionin Guzzle 7, so it was retried, but it is a plainNetworkExceptionin Guzzle 8, so it no longer is. Making retries configurable per request is a possible follow-up; it is not part of this PR.Verification
Unit tests and PHPStan (
analyse src, level 5) were run in containers. Nocomposer.lockwas used, as in CI.--with guzzlehttp/guzzle:^7.9)--prefer-lowest(Guzzle 7.15.2, Promises 2.5.1, PSR-7 2.13.0)End-to-end checks used the real cURL handler against PHP's built-in server; no external API was called.
timeout => 0.3.ConnectExceptionafter 4 attempts, 8.2 s;NetworkTimeoutExceptionafter 4 attempts, 8.2 s;OpenAICompatibleClientagainst a local SSE endpoint that sends the chunks with 150 ms pauses, then a plain request. Identical on Guzzle 7 and 8:Review notes
Utils::unwrap/Create;Pool;ClientException::getResponse();ResponsefromgetHeaders();eof()/read()loop.$headersargument ofHttpClientFactory::createClient();read_timeoutto 60 s.Test environment
None: this is a library with no
*.simplia.netenvironment. It was verified by the unit-test matrix and the local end-to-end runs above.Upgrade review
Range: guzzlehttp/guzzle 7.16 → 8.3 (8.0.0–8.3.0), guzzlehttp/promises 2.6 → 3.1, guzzlehttp/psr7 2.13 → 3.2. Read: Guzzle
UPGRADING.md"7.0 to 8.0" (tag 8.3.0); the promises and psr7UPGRADING.md"3.0" sections (branch 3.0).Breaking / behaviour changes affecting us:
RetryMiddleware::exponentialDelay()removed → local delay function;NetworkTimeoutException→ retried explicitly;NetworkException: kept unretried;ConnectException: accepted, documented above;getHandlerContext()removal (unused),RequestException::getResponse()removal (we only call it onClientException), stream and multipart changes.Deprecated APIs we used:
RetryMiddleware::exponentialDelay(), deprecated in 7.11 → replaced. Greppedsrc/for every Guzzle, Promises and PSR-7 symbol.Workarounds removed: none. Searched for
workaround,TODOand Guzzle version comments.Adopted improvements: none. Reviewed Guzzle 8's generic
PromiseInterface<T>PHPDoc; adopting it would needPromiseInterface<LLMResponse>annotations across the clients and would only type-check on Promises 3.Deferred improvements:
read_timeoutfor streaming.These are proposals for the maintainer; no issues are filed.
🤖 Generated with Claude Code