fix(rest/python): assign checkout id server side instead of trusting the request - #167
Open
vishkaty wants to merge 1 commit into
Open
fix(rest/python): assign checkout id server side instead of trusting the request#167vishkaty wants to merge 1 commit into
vishkaty wants to merge 1 commit into
Conversation
…the request
A schema-valid checkout create carrying a non-string top-level `id`
returns HTTP 500. checkout.json annotates `id` with `ucp_request: omit`
(the business assigns it), so the generated CheckoutCreateRequest
declares no id field; extra="allow" then admits a client-sent `id` of
any JSON type as an extra member, keeping the request schema-valid.
create_checkout read that extra (`getattr(checkout_req, "id", None)`)
and passed it verbatim into the Checkout response model, where a
non-string raised an uncaught pydantic ValidationError:
id
Input should be a valid string [type=string_type, input_value=123]
Observed vs expected, reproduced against main:
POST /checkout-sessions
UCP-Agent: profile="https://spck.dev/agent"
idempotency-key: <key>
request-id: <id>
Content-Type: application/json
{"id":123,"currency":"USD","line_items":[{"id":"li_1","quantity":1,
"item":{"id":"bouquet_roses","price":1000},"totals":[]}],
"payment":{"instruments":[],"handlers":[]},"status":"incomplete",
"ucp":{"version":"2026-04-08"},"totals":[],"links":[]}
-> 500 Internal Server Error (expected: 201)
Controls: the same request with `id` omitted, or with a string id,
returns 201.
This is the same defect class as the currency read fixed in Universal-Commerce-Protocol#156: the
server determines an omit field and never takes it from the request.
The create path now always assigns its own uuid and ignores any
client-sent id. The update path never read the body id (the id comes
from the URL path), so it has no analogue of this defect.
Accounting for the other omit-annotated checkout fields at 2026-04-08
(status, totals, links, ucp, expires_at, currency): each is either
excluded from the request dump before the response model is constructed
or already determined server side (currency since Universal-Commerce-Protocol#156), and wrong-typed
values for each were verified to return 201 on unmodified main.
Why the existing suite missed it: every lifecycle test built its payload
through _create_checkout_payload, which always sets a string id, and
then addressed follow-up calls with that same id, so a non-string id was
never sent and the id echo was baked into the tests as a contract. Those
tests now use the server-returned id, matching what the happy-path
client already does, and a new test covers non-string, string, and
omitted id on create.
carolinerg1
approved these changes
Aug 6, 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.
Observed
POST /checkout-sessionsreturns HTTP 500 when the request carries a non-string top-levelid.idis annotateducp_request: omit(server-assigned), so a request that includes it is schema-valid and a conformant platform may send anything there — but the reference crashes on a non-string value.Full reproducing request (a spec-valid create; the id is the only variable):
Controls:
idomitted → 201,id:"x"(string) → 201.Root cause: the generated
CheckoutCreateRequestdeclares noidfield (omit at 2026-04-08) butextra="allow"admits a clientidof any type;create_checkoutread it back withgetattr(checkout_req, "id", None)and passed it intoCheckout(id=…), whoseidis a required string, raising an uncaught pydanticValidationError.This is the direct sibling of #156 (which fixed the same class for
currencyby determining it server-side instead of reading it from the request).Fix
services/checkout_service.py— assign the id server-side unconditionally instead of trusting the request:The update path has no analogue (it takes the id from the URL, never a body id). Class check: every other server-managed omit field (
status,totals,links,ucp,expires_at, andcurrencyfrom #156) already returns 201 on a wrong-typed value;idwas the last one reading the request unsanitized.Verification
test_create_assigns_id_server_side(integration_test.py, alongside fix(rest/python): determine currency server side instead of reading it from the create request #156's omit tests) fails on the old code with the exactValidationError, passes after; kill-tested.uv run pytest: 135 passed. Happy-path client end to end: exit 0. Pinned pre-commit (ruff + ruff-format) clean.Note
A few existing lifecycle tests were reusing the client-chosen id in follow-up URLs; they now use the server-returned id (what the happy-path client already did). There is an adjacent class of unrelated extras (e.g.
continue_url,order,messages,platform) that still 500 by colliding with response-only fields — out of scope here; happy to send a follow-up that builds the checkout from declared request fields only.