Replace ClientException with three exceptions behind one ExceptionInterface - #18
Merged
Merged
Conversation
…erface The single ClientException flattened Meta's structured error into a message, so a caller could not tell a transient error from a permanent one without parsing text. It also promised to cover "any" request failure while the PSR-18 exception of the HTTP client and a JsonException escaped unwrapped, and it made a caller mistake such as a missing access token a RuntimeException. Everything the SDK throws now implements ExceptionInterface, and there is one concrete class for each thing a caller can do about a failure: - InvalidArgumentException (extends SPL's): the caller has to fix the input, never retry. Thrown for a cookie value in the wrong format, invalid event data, a pixel without an access token and a payload that cannot be encoded. - TransportException: no response at all, retry. Wraps the PSR-18 exception. - ResponseException: a non-200 response. Carries the status code, the raw body and, when the body is in Meta's error format, the ErrorResponse with code, subcode, type, trace id and the transient flag. ErrorResponse is no longer internal and is now readonly. An internal Assert subclass makes every failed assertion in src/ throw the SDK's InvalidArgumentException, and the exception of the Facebook normalizer is wrapped with the name of the field added. FbqGenerator::generateTrack() no longer lets a JsonException escape: it logs and returns an empty string, like generateInit() already did, since the output of both goes straight into a page.
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.
Summary
The SDK had a single exception,
ClientException, with three problems:ErrorResponseparsed the code, subcode, type, trace id andis_transient, and the exception then threw all of it away into the message. A queue handler deciding whether to retry had to parse text, and the HTTP status code was not available at all.@throws ClientException if the request failed in any way, yet the PSR-18 exception of the HTTP client (timeouts, DNS) and a\JsonExceptionescaped unwrapped, and so did the SPL / webmozart exceptions thrown while building the payload insendEvent().\RuntimeExceptionbase class it made a caller mistake such as a missing access token a runtime error. Its name also collides with PSR-18'sClientExceptionInterface.Design
Everything the SDK throws implements
ExceptionInterface, and there is one concrete class for each thing a caller can do about a failure:InvalidArgumentException(extends SPL's)TransportExceptionpreviousResponseExceptionstatusCode,bodyand?ErrorResponseHow the "everything" promise is kept:
Setono\MetaConversionsApi\Assert(@internal) extends webmozart'sAssertand overridesreportInvalidArgument(), so every failed assertion insrc/throws the SDK'sInvalidArgumentException. PHPStan's type narrowing keeps working, since the assertion methods are still declared on the parent.Fbc::fromString()/Fbp::fromString()throw it directly.\InvalidArgumentExceptionof the FacebookNormalizeris wrapped, and the message now names the field:The value of the field "action_source" is invalid: ….\JsonExceptionfrom encoding the payload.ErrorResponseis no longer@internal, since it is exposed throughResponseException::$errorResponse. Its properties are readonly andfromJson()throwsInvalidArgumentException.FbqGenerator::generateTrack()no longer lets a\JsonExceptionescape. It logs and returns an empty string, which is whatgenerateInit()already did; the output of both goes straight into a page.Because the SDK's
InvalidArgumentExceptionextends PHP's, existingcatch (\InvalidArgumentException $e)blocks, e.g. aroundFbc::fromString(), keep working.Deliberately not wrapped:
Http\Discovery\Exception\NotFoundExceptionwhen no PSR-18 client is installed. That is a setup problem with a good message of its own.Backwards compatibility
Intentional breaks, all documented in
UPGRADE-2.0.mdwith a 1.x to 2.0 mapping table:ClientExceptionis removed, PSR-18 exceptions no longer escape from the client,ErrorResponsechanged shape, andgenerateTrack()no longer throws. The Roave BC job is red by design.Docs
README ("Error handling" rewritten around the three classes, plus notes in the queue, cookie and browser-side sections), CLAUDE.md (new Exceptions paragraph including the rule to use the internal
Assertand wrap third-party exceptions),UPGRADE-2.0.md.Test plan
ResponseExceptionwith Meta's error (status code, body, parsed error), with a non-JSON body from a proxy (errorResponsenull,previousset), stops at the first failing pixel,TransportExceptionwrapping the HTTP client's exception,InvalidArgumentExceptionfor an unencodable payload and for missing access tokens. The exceptions are caught throughExceptionInterfaceand then asserted to be the concrete classInvalidArgumentExceptionTest: catchable as the SDK interface, the SDK class and the SPL class; thrown by a failed assertionResponseExceptionTest(all message variants, null error response),TransportExceptionTestEvent,Content,CustomandErrorResponsetests now expect the SDK's exception; the invalidaction_sourcetest asserts the field name in the messageFbqGeneratorTest:generateTrack()logs and returns an empty string for unencodable custom data