Skip to content

Support Guzzle 8 while keeping Guzzle 7 compatibility - #880

Open
mokuzon wants to merge 5 commits into
masterfrom
users/mokuson/NO-ISSUE/fix-guzzle8-ci
Open

Support Guzzle 8 while keeping Guzzle 7 compatibility#880
mokuzon wants to merge 5 commits into
masterfrom
users/mokuson/NO-ISSUE/fix-guzzle8-ci

Conversation

@mokuzon

@mokuzon mokuzon commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Supersedes #876 (guzzlehttp/guzzle to v8).

Its CI fails because laravel/framework (via dev dependency orchestra/testbench) still requires Guzzle ^7.8.2, so pinning the root requirement to ^8.0 makes composer install unresolvable.

Changes

@mokuzon mokuzon changed the title NO-ISSUE: Support Guzzle 8 while keeping Guzzle 7 compatibility Support Guzzle 8 while keeping Guzzle 7 compatibility Jul 28, 2026
@mokuzon
mokuzon force-pushed the users/mokuson/NO-ISSUE/fix-guzzle8-ci branch from 0f6da5c to 74c6695 Compare July 28, 2026 01:08
@mokuzon mokuzon self-assigned this Jul 28, 2026
@mokuzon
mokuzon force-pushed the users/mokuson/NO-ISSUE/fix-guzzle8-ci branch 4 times, most recently from a9ae9ca to 55acb61 Compare July 28, 2026 05:02
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this catch structure changed: Guzzle 8 removed RequestException::getResponse(), which the previous code relied on. The response-carrying exceptions were moved to a new ResponseException subclass:

  Guzzle 7                                Guzzle 8
  ────────────────────────────────        ────────────────────────────────
  TransferException                       TransferException
  ├── RequestException                    ├── RequestException
  │    │  getResponse(): ?Response        │    │  (getResponse() removed!)
  │    └── BadResponseException           │    └── ResponseException  (new)
  │          4xx/5xx, response            │          │  getResponse(): Response
  │          guaranteed                   │          └── BadResponseException
  └── ConnectException                    └── NetworkException  (new)
                                               └── ConnectException

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want the equivalent exception, BadResponseException is not the right one. You should still catch RequestException and check if it's an instance of ResponseException to see if you can get the response headers or body.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in 22eb61a.

It now catches RequestException again, but guards with method_exists($e, 'getResponse') instead of instanceof ResponseException, since ResponseException doesn't exist in Guzzle 7 and this SDK supports both majors: the guard is true for every RequestException on Guzzle 7 and for the ResponseException subtree on Guzzle 8.

Comment on lines +465 to +472
if (!$exception instanceof BadResponseException) {
throw new ApiException(
"[{$exception->getCode()}] {$exception->getMessage()}",
(int) $exception->getCode(),
null,
null
);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original callback called $exception->getResponse() unconditionally, but rejections without a response (e.g. ConnectException, which has no getResponse()) made this crash with a fatal Error even on Guzzle 7 as a bug.

This branch converts such failures to a response-less ApiException, exactly as the sync path's catch (ConnectException) has always done.

@mokuzon
mokuzon marked this pull request as ready for review July 28, 2026 05:46
@mokuzon
mokuzon requested a review from a team July 28, 2026 05:46
$options = $this->createHttpClientOption();
try {
$response = $this->client->send($request, $options);
} catch (RequestException $e) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want the equivalent exception, BadResponseException is not the right one. You should still catch RequestException and check if it's an instance of ResponseException to see if you can get the response headers or body.

@mokuzon
mokuzon force-pushed the users/mokuson/NO-ISSUE/fix-guzzle8-ci branch from 2e1ace6 to 41e6f91 Compare August 7, 2026 05:07
renovate Bot and others added 5 commits August 7, 2026 14:17
Update the api.pebble template for two Guzzle 8 breaking changes:

- Replace GuzzleHttp\Utils::jsonEncode(), removed in Guzzle 8, with native
  json_encode() using JSON_THROW_ON_ERROR.
- Stop calling RequestException::getResponse(), which was moved to
  ResponseException in Guzzle 8. Catch BadResponseException for error
  responses instead, which carries the response in both Guzzle 7 and 8,
  and TransferException for failures without a response such as connection
  errors (both in sync and async paths).

Also widen the guzzlehttp/guzzle constraint to ^7.3 || ^8.0.
orchestra/testbench (via laravel/framework, which still requires guzzle
^7.8.2) cannot be installed together with guzzle ^8.0 only, which made
composer install unresolvable and broke CI.
Generated by python3 generate-code.py from the updated api.pebble
template. No manual edits.
Review feedback from the Guzzle maintainer: catching BadResponseException
is not equivalent to the pre-Guzzle-8 behavior. Response-carrying
failures that are not 4xx/5xx errors, such as TooManyRedirectsException
(Guzzle 7: extends RequestException, Guzzle 8: extends ResponseException),
would lose their response headers and body.

Go back to catching RequestException as before, and take the response
when the exception can provide one. In Guzzle 7 every RequestException
has getResponse(); in Guzzle 8 only the ResponseException subtree does,
so guard the call with method_exists(), which expresses exactly that
availability in both majors. Apply the same rule to the async rejection
callback.
Generated by python3 generate-code.py from the updated api.pebble
template. No manual edits.
@mokuzon
mokuzon force-pushed the users/mokuson/NO-ISSUE/fix-guzzle8-ci branch from 41e6f91 to e72f9c2 Compare August 7, 2026 05:18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add tests to verify that the changed code path works with both v7 and v8?

Reading the v7 and v8 code to confirm the behavior is great, but having tests would give us stronger confidence that the implementation is correct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants