Repository navigation
fix(rest/python): answer five client reachable faults with 4xx or a result instead of a bare 500 - #257
Merged
Conversation
…esult instead of a bare 500 Five requests reach code that raises outside the two registered exception handlers (RequestValidationError and UcpError), so FastAPI answers each with its default "Internal Server Error" text body: 1. a create that carries a `platform` member: the request dump is unpacked with **checkout_data while platform= is also passed as a keyword, so UnifiedCheckout() raised TypeError. `platform` joins the exclude set. 2. an agent profile whose webhook_url is not a URL: the raw value reached PlatformSchema, whose webhook_url is AnyUrl, and raised ValidationError on every create by that agent. The value is now gated through the UcpConfig model the module already declares, and dropped with a log line, which is how the function already treats every other profile problem. 3. a complete whose card credential carries a non string `number`: the last four digits slice raised TypeError. It is now INVALID_REQUEST 400. 4. an update that reuses a platform chosen destination id with new content, or another buyer reusing the id: the id is the primary key of customer_addresses and only content was looked up, so the insert raised IntegrityError. The row is updated in place for its owner and a fresh key is assigned when another customer holds the id. The response still carries the id the platform sent, because the row key is internal and echoing a different id would leave selected_destination_id matching no destination, which stalls the checkout more quietly than the error. 5. a create that carries an `ap2` member: it is not declared on the create request so a client value survives as an extra, and it IS a declared field on the response model, so a wrong typed value raised ValidationError there. The schema annotates the member create: omit and the server never sets it on create, so `ap2` joins the exclude set too. Each case has a test that fails with the original exception before the fix and passes after it.
damaz91
approved these changes
Oct 19, 2026
damaz91
left a comment
Contributor
There was a problem hiding this comment.
Thanks @vishkaty! Rebased onto main to resolve the integration_test.py conflict and updated dest_data.setdefault("id", saved_id) to if not dest_data.get("id"): dest_data["id"] = saved_id so empty-string destination IDs ("id": "") continue to adopt saved_id in conformance tests. LGTM!
nicholasjameshall
approved these changes
Oct 19, 2026
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.
Five client or agent reachable requests answer
500 text/plain Internal Server Errorinsteadof a 4xx envelope or a result. Each reaches code that raises a type no handler covers:
server.pyregisters handlers forRequestValidationErrorandUcpErroronly, so anythingelse reaches the framework default.
This class has been fixed one site at a time before, in #156, #167, #196 and #220. The five
below are not covered by those. Two sibling conditions in the same class are already covered by
open pull requests and are deliberately left out here: a malformed
paymentbody on complete(#240) and the idempotency key
IntegrityError(#245).Probed on a freshly cloned and seeded reference at main
b7d7a549, with the agent profileserved by a local stub.
platformmemberTypeError: models.UnifiedCheckout() got multiple values for keyword argument 'platform'ap2member of the wrong typeValidationErrorfor theap2field on the response modelwebhook_urlValidationError: 1 validation error for PlatformSchemanumberthat is an integerINVALID_REQUESTTypeError: 'int' object is not subscriptableidwith new contentsqlite3.IntegrityError: UNIQUE constraint failed: customer_addresses.idControls, same run: a plain create is 201; the same profile with a valid
webhook_urlis 201;the same card
numberas a string is already a correct 400 envelope; a first update with thatdestination id is 200.
Why each happens.
create_checkoutunpacks the request dump with**checkout_datawhile also passingplatform=, andplatformwas the one keyword missing from the exclude set that exists forexactly this reason. Nearly every SDK model is configured
extra="allow"(189 of 192), so aclient sent member survives into the dump.
checkout.jsonat v2026-04-08 declares noplatformmember, so this is an unknown member a server should tolerate rather than a field it may trust.
ap2reaches the same line by a different route. It is not declared on the create request, so aclient value survives as an extra; it is a declared field on the response model through the AP2
mixin, so a wrong typed value is validated and rejected there. The service never sets it on
create, and the schema annotates the member as omitted on create, so excluding it is safe. One
consequence beyond the error: a well formed client
ap2was previously stored and echoed back oncreate, and is now dropped there. That follows the annotation, and the complete path, where the
mandate is the one that matters, is untouched.
extract_webhook_urlreturns the raw profile value and the route feeds it toPlatformSchema,whose
webhook_urlis a required URL type. The module already declares aUcpConfigmodel withHttpUrlfor this purpose and does not use it. This fires on every create or update by thatagent, not only on create.
_process_paymentslicesnumber[-4:]for a log line, and the credential model the route buildsadmits extras of any type.
save_customer_addressuses the platform chosen destination id as the primary key ofcustomer_addressesand looks rows up by content only, so a second use of one id collides.Why the suite did not catch it. The existing integration tests send well formed members and a
valid profile, so none of the five conditions is constructed anywhere. Each fix below ships with
the test that reproduces it.
The fixes, 48 added and 2 removed lines of production code across 7 hunks in 3 files.
platformandap2join the exclude set. The profile value is gated through theUcpConfigmodel the module already declares and dropped with a log line when it fails, which is how
extract_webhook_urlalready treats every other profile problem. A non string card number raisesInvalidRequestError.save_customer_addressupdates the row in place when the same customerreuses an id, and takes a fresh internal key when another customer already holds that id.
In that last case the response still carries the id the platform sent. The row key is internal,
and echoing a different id back would answer 200 while leaving
selected_destination_idpointingat no destination, so no shipping option is quoted and the checkout cannot reach
ready_for_complete. A test asserts the id is echoed unchanged in that case. One edge is worth naming: when thesame buyer sends new content that already exists under another of their own ids, the content match
still wins and the stored row for the other id keeps its own content, so a later listing shows it
unchanged. Reordering the two checks would create two rows with identical content for one
customer, which the single row lookup would then reject, so the match order is left alone.
One thing to disclose about the
webhook_urlgate:UcpConfigusesHttpUrl, which is stricterthan the
AnyUrlonPlatformSchema. A value such as anftpURL, or an https URL long enoughto exceed the length bound, is stored today and would be dropped with a log line instead. Nothing
crashes either way, and if gating on
PlatformSchemaitself is preferred so the two agreeexactly, that is a one line change.
One of these is a contract choice rather than a crash to remove. For the malformed
webhook_urlthe patch drops the value, matching the lenient handling of every other profile fault in that
function. The Error Codes table maps
profile_malformedto 422, so a hard error is alsodefensible. Happy to switch to 422 if that is preferred.
Tests. Six cases added to
rest/python/server/integration_test.pyin the existing idiom.Against the unfixed tree they fail with the exceptions above. With the fixes they pass. Each fix
was then excised with its test kept, and each test went red alone, so none of them passes without
the code it covers.
Checks run locally on this branch.
uv run pytest -vinrest/python/server: 307 passed and12 subtests passed, against 283 and 12 on main. The difference is the six new cases collected four
times each, because
IntegrationTestis imported bycart_test.py, where it is collected bothunder its own name and through a subclass, and by
signature_integration_test.py. The seededserver boots and
simple_happy_path_client.pycompletes successfully. The pinnedpre-commit run --all-filespasses all hooks. The upstreampython-server-testsworkflow was alsorun against this exact branch and passed. Re-probing the five requests against the fixed server
gives 201, 201, 201, the 400 envelope, and 200.
The nodejs twin was probed on the wire for all five. It answers 201 for the platform member
and for a wrong typed
ap2, and 200 for the reused destination id, so those do not arise there.The integer card number is neither validated nor read: the generated credential schema declares
only
typeand is not strict, so validation passes withnumberpresent, and the completehandler reads only
type. The outcome is identical to a string number. For the malformedwebhook_urlit answers 201 and stores the raw invalid value, which looks like a separatequestion for that server rather than part of this change.