PMK-2865: stop leaking Guzzle's exception hierarchy - #169
Open
ewood-ac wants to merge 1 commit into
Open
Conversation
processRestRequest() documented @throws GuzzleException and left the request call unguarded, so the HTTP client's exception types were part of this SDK's public contract. That coupling was total rather than partial: because the SDK sets http_errors => false and maps responses to PostmarkException itself, the transport family is the ONLY Guzzle family that can reach a caller. Guzzle 8 reclassified exactly that family, and PMK-2061 widens the constraint to allow it. Composer resolves highest, so upgrading moves callers onto Guzzle 8 whether or not they ask -- and catch (ConnectException) around a send stops matching a timeout, silently, with no code change on their side. Transport failures are now PostmarkTransportException extends PostmarkException, so existing catch (PostmarkException) keeps working and the original is on getPrevious(). isTimeout() and isConnectionFailure() let callers make retry decisions without importing anything from GuzzleHttp. All version-specific knowledge is confined to one private classifier, because the two majors are less similar than they look -- established by inspecting the installed packages rather than the docs: Guzzle 7 ConnectException covers connect AND timeout, and carries the cURL errno on getHandlerContext(), which is what separates them. Guzzle 8 ConnectException is re-parented under NetworkException, DROPS getHandlerContext() entirely, and adds ConnectTimeoutException, NetworkTimeoutException and ResponseTimeoutException instead. So errno-based classification alone would have silently degraded to "unknown" on Guzzle 8 -- caught because the tests run under both majors, not because it was predicted. The classifier matches on short class name first, falls back to errno, then to the message, and never references a class that exists in only one major. Deliberately unchanged: http_errors stays false. Flipping it would bypass the body parsing that produces Postmark's own ErrorCode/Message, and would ADD Guzzle's response-exception family to the caller surface -- the opposite of this change. Verified green under both 8.0.2 and 7.15.2: PHPStan clean, 97 tests, 0 failures. The PMK-2061 CI matrix runs both.
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.
Jira: PMK-2865
Stacked on #168 (PMK-2061 / v8.0.0) — review that first. Ships in v8.0.0, not a later major.
Problem
processRestRequest()documented@throws \GuzzleHttp\Exception\GuzzleExceptionand left the request call unguarded, making the HTTP client's exception hierarchy part of this SDK's public contract.That coupling is total, not partial: because the SDK sets
http_errors => falseand maps responses toPostmarkExceptionitself, the transport family is the only Guzzle family that can reach a caller. It's 100% of the leaked surface.Guzzle 8 reclassified exactly that family, and #168 widens the constraint to allow it. Composer resolves highest, so upgrading moves callers onto Guzzle 8 whether they ask or not — and this silently stops matching:
Change
Transport failures — DNS, connect, TLS, timeout, socket — become
Postmark\Models\PostmarkTransportException, which extendsPostmarkException, so existingcatch (PostmarkException $e)blocks keep working unchanged. The original Guzzle exception is preserved ongetPrevious().The two majors are less alike than they look
All version-specific knowledge lives in one private classifier. This was established by inspecting the installed packages, not by reading docs:
ConnectExceptionparentTransferExceptionNetworkExceptiongetHandlerContext()(the cURL errno)ConnectException+ errno 28NetworkTimeoutException/ConnectTimeoutException/ResponseTimeoutExceptionAn errno-based classifier would have silently degraded to "unknown" on Guzzle 8 — and that was caught because the tests run under both majors, not because it was predicted. The classifier now matches short class name first, falls back to errno, then to the message, and never references a class that exists in only one major.
Deliberately unchanged:
http_errorsstaysfalseRaised during review and rejected on two grounds. It would bypass the body parsing that produces Postmark's own
ErrorCode/Message— everycatch (PostmarkException)would get a generic Guzzle error with no API detail. And it would add Guzzle's response-exception family (ClientException/ServerException, also re-parented in 8) to the caller surface, which is the opposite of this change.http_errors => falseis what has been insulating this SDK from Guzzle 8's loudest break.Verification
Green under both majors — PHPStan
[OK], 97 tests, 0 failures on 8.0.2 and 7.15.2. The Guzzle matrix added in #168 runs both.New
tests/TransportExceptionTest.php(credential-free) covers: wrapping, catchability asPostmarkException, classification without importing GuzzleHttp, the no-errno fallback, and that a 401 is still a plainPostmarkException— a response that arrives is not a transport failure.Breaking change
catch (GuzzleException)around SDK calls stops matching. Documented in the v8 upgrade notes with the before/after. Anyone catchingPostmarkExceptionis unaffected.