Skip to content

Support Guzzle 8 - #368

Open
wit3 wants to merge 1 commit into
omniphx:masterfrom
wit3:feat/guzzle-8
Open

wit3 wants to merge 1 commit into
omniphx:masterfrom
wit3:feat/guzzle-8

Conversation

@wit3

@wit3 wit3 commented Oct 12, 2026

Copy link
Copy Markdown

Guzzle 8.0 is out, and Laravel 13 already accepts it (guzzlehttp/guzzle: ^7.8.2 || ^8.0). Forrest's ~6.0|~7.0 is now the constraint that keeps applications on Guzzle 7. Widening it isn't enough on its own, though: against Guzzle 8.2.0, unpatched master breaks in three places.

  1. Client::assignExceptions() fatals on every error response. Guzzle 8 moved the response from RequestException to the new ResponseException subclass, and RequestException no longer has hasResponse()/getResponse(). Every 4xx/5xx fails with "Call to undefined method", including the 401 that should trigger the token refresh.
  2. SalesforceException / TokenExpiredException can't be built. They pass $e->getResponse() as the third argument of RequestException::__construct(), which in Guzzle 8 is int $code (TypeError). ResponseException's constructor is final, so extending it isn't an option.
  3. HTTP methods are sent lowercase. Guzzle 8 preserves the method casing it is given. Forrest passes 'get'/'post' (and the user-supplied 'method' => 'patch'), so they would reach Salesforce verbatim.

Changes

  • composer.json: guzzlehttp/guzzle: ~6.0|~7.0|^8.0.
  • Methods are uppercased in handleRequest(), and the literal 'get'/'post' in Client and the authentication classes are now 'GET'/'POST'. Nothing changes on Guzzle 6/7, which already uppercased them on the wire.
  • New Exceptions\WrapsRequestException trait, used by SalesforceException and TokenExpiredException:
    • Both classes still extend RequestException and keep their ($message, RequestException $e) constructor.
    • They store the response themselves and expose the same getResponse(): ?ResponseInterface and hasResponse(): bool, so catch blocks that call $e->getResponse() keep working on both majors.
    • The parent constructor call is chosen by class_exists(ResponseException::class).
    • The exception code is still the HTTP status.
  • assignExceptions() reads the response through WrapsRequestException::responseFrom(), which handles both shapes.

Tests

  • TestCase::requestException() uses RequestException::create(), which exists on both majors.
  • Unit tests that expected lowercase methods now expect uppercase.
  • Three new tests in ClientTest drive a real GuzzleHttp\Client (MockHandler + history middleware) instead of a mocked ClientInterface. The mocked tests never reach Guzzle's request pipeline, which is why 45/46 of them still passed on Guzzle 8 while the library was broken. The new tests cover:
    • 401 → refresh → retry, with GET on both requests;
    • a 400 wrapped in SalesforceException with getResponse(), hasResponse(), getCode(), the message and getPrevious();
    • 'method' => 'patch' sent as PATCH.

Local runs on PHP 8.5:

master this PR
Guzzle 7.15.5 ✅ ✅ 49 tests
Guzzle 8.2.0 ❌ 8 failing (with these tests) ✅ 49 tests

No workflow change is needed: the existing matrix already resolves Guzzle 7 on Laravel 11/12 (^7.8.2) and Guzzle 8 on Laravel 13.

Not covered here: UserPasswordSoap has no test that goes through a real client. Its only change is 'post' → 'POST'.

- Allow guzzlehttp/guzzle ^8.0 alongside ~6.0|~7.0.
- Send HTTP methods uppercase: Guzzle 8 preserves the method casing it is
  given, so the lowercase 'get'/'post' (and the 'method' option, e.g.
  'patch') would reach Salesforce verbatim.
- Read the response of a failed request on every major: Guzzle 8 moves it
  from RequestException to ResponseException and drops hasResponse(), so
  assignExceptions() failed with "call to undefined method" on every
  4xx/5xx, including the 401 that triggers the token refresh.
- SalesforceException and TokenExpiredException keep extending
  RequestException and keep exposing getResponse()/hasResponse(); they
  store the response themselves because Guzzle 8's RequestException
  constructor no longer takes one (and ResponseException's is final).
- Tests: build RequestException through the create() factory, which exists
  on both majors, and cover the request pipeline with a real Guzzle client
  (MockHandler + history): 401 -> refresh -> retry, error response wrapped
  in SalesforceException, uppercase method on the wire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant