Skip to content

[4.x] Fix Guzzle 8 transfer exceptions in GuzzleSender - #569

Merged
Sammyjo20 merged 3 commits into
saloonphp:v4from
JonPurvis:fix/568-guzzle-8-transfer-exceptions
Oct 25, 2026
Merged

Sammyjo20 merged 3 commits into
saloonphp:v4from
JonPurvis:fix/568-guzzle-8-transfer-exceptions

Conversation

@JonPurvis

@JonPurvis JonPurvis commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #568

Thanks @intrepidws for the clear report and reproduction script.

Problem

Since Guzzle 8 (#561), GuzzleSender::send() still only caught ConnectException and RequestException. That left two gaps:

  1. Network failures before headers (NetworkException, NetworkTimeoutException, etc.) are no longer ConnectException, so synchronous send() let raw Guzzle exceptions escape. Saloon’s fatal pipeline and connector $tries retries never ran (send() only retries FatalRequestException|RequestException).
  2. Failures after headers (ResponseTransferException, e.g. ResponseTimeoutException) are RequestExceptions with a partial response, so Saloon returned a normal Response (status 200, failed(): false) instead of treating the transfer as fatal.

sendAsync() already wrapped (1) via processPromise(); both paths were affected by (2).

Reproduction

saloonphp/saloon v4.5.0 + guzzlehttp/guzzle 8.3.0, connector $tries = 3, MockHandler throwing Guzzle 8 exception types (same script as the issue).

Before (v4.5.0)

Exception Mode Outcome
ConnectException send attempts: 3 → FatalRequestException
ConnectException sendAsync attempts: 1 → FatalRequestException
NetworkTimeoutException send attempts: 1 → GuzzleHttp\Exception\NetworkTimeoutException
NetworkTimeoutException sendAsync attempts: 1 → FatalRequestException
NetworkException send attempts: 1 → GuzzleHttp\Exception\NetworkException
NetworkException sendAsync attempts: 1 → FatalRequestException
ResponseTimeoutException send attempts: 1 → returned Response (200, failed: false, partial body)
ResponseTimeoutException sendAsync attempts: 1 → returned Response (200, failed: false, partial body)

After (this PR)

Exception Mode Outcome
ConnectException send attempts: 3 → FatalRequestException
ConnectException sendAsync attempts: 1 → FatalRequestException
NetworkTimeoutException send attempts: 3 → FatalRequestException
NetworkTimeoutException sendAsync attempts: 1 → FatalRequestException
NetworkException send attempts: 3 → FatalRequestException
NetworkException sendAsync attempts: 1 → FatalRequestException
ResponseTimeoutException send attempts: 3 → FatalRequestException
ResponseTimeoutException sendAsync attempts: 1 → FatalRequestException

Note: sendAsync still shows 1 attempt because connector $tries only applies to synchronous send(), not sendAsync(). That behaviour is unchanged.

Solution

  • Catch remaining TransferException in send() and wrap as FatalRequestException (aligned with processPromise()).
  • Shared responseFromRequestException() for sync + async: treat Guzzle 8 ResponseTransferException as fatal; keep existing behaviour for response-less RequestException and normal HTTP error responses.
  • class_exists(ResponseTransferException::class) so Guzzle 7 is unaffected.

Tests

  • tests/Feature/Guzzle8SenderExceptionTest.php (skipped when Guzzle 8 exception classes are not present).

Test plan

  • Pest: Guzzle8SenderExceptionTest (on Guzzle 8)
  • Existing RequestExceptionTest / GuzzleSenderTest on Guzzle 7
  • Manual repro: before/after tables above

@JonPurvis JonPurvis changed the title fix guzzle 8 transfer exceptions [4.x] Fix Guzzle 8 transfer exceptions in GuzzleSender Oct 23, 2026

@Sammyjo20 Sammyjo20 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @JonPurvis !

@Sammyjo20
Sammyjo20 merged commit 6e6932a into saloonphp:v4 Oct 25, 2026
18 checks passed
@JonPurvis

Copy link
Copy Markdown
Contributor Author

You're welcome! Thanks to @intrepidws for reporting!

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.

GuzzleSender::send() doesn't handle Guzzle 8's NetworkException or ResponseTransferException

2 participants