Skip to content

fix(rest/nodejs): answer checkout errors with the UCP error envelope - #174

Open
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/nodejs-error-envelope
Open

fix(rest/nodejs): answer checkout errors with the UCP error envelope#174
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/nodejs-error-envelope

Conversation

@vishkaty

@vishkaty vishkaty commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Observed vs expected

The Node reference answers checkout business and protocol failures with a flat {"detail": "..."} body. The Python reference answers the same failures with the UCP error envelope through its typed exception taxonomy (rest/python/server/exceptions.py + the ucp_exception_handler in server.py).

Reproduced on the wire at current main (5237750), both cases against a freshly booted server:

  • Reuse an Idempotency-Key with a different create body: 409 {"detail":"Idempotency key reused with different parameters"} with no code and no content. checkout-rest.md (Protocol errors) expects the JSON body to carry code and content.
  • Create a checkout whose only line is a zero stock product (gardenias): 400 {"detail":"Insufficient stock for item gardenias"} with no ucp envelope. checkout.md (Error Handling) expects ucp.status: "error" with messages[] describing the failure. The Python server self tests exactly this shape (server/integration_test.py, out of stock asserts ucp.status == "error").

Fix

New src/utils/ucp_error.ts mirrors the Python taxonomy, and every checkout error site converts to it. HTTP statuses are preserved at every site; this is a response shape change only.

Sites (create / update / complete / cancel) Code Status Python counterpart
Idempotency key reused, 4 sites IDEMPOTENCY_CONFLICT 409 IdempotencyConflictError
Checkout session not found, 4 sites RESOURCE_NOT_FOUND 404 ResourceNotFoundError
Update / complete / cancel a terminal checkout, 3 sites CHECKOUT_NOT_MODIFIABLE 409 CheckoutNotModifiableError
Insufficient stock (create / update via typed validateInventory throw; reservation at complete) OUT_OF_STOCK 400; 409 at complete OutOfStockError (default 400; complete path raises 409)
Missing product id, unknown product, missing payment data or credentials, unsupported handler, fulfillment not selected INVALID_REQUEST 400 InvalidRequestError
Mock payment declines INSUFFICIENT_FUNDS / FRAUD_DETECTED / UNKNOWN_TOKEN, severity requires_buyer_input 402 / 403 / 400 PaymentFailedError

Deliberately left as is: the signature.ts 401 wrapper (its own documented shape, identical in both languages), the internal 500 in completeCheckout (Python leaves unhandled errors to the plain FastAPI 500 as well), and the testing.ts simulation endpoints. One asymmetry there worth naming honestly: the Simulation-Secret 403 is flat HTTPException detail in Python too, but Python does envelope the order-not-found 404 on simulate shipping (it flows through ResourceNotFoundError), while testing.ts answers it flat. Converting the simulation surface felt like scope creep for this change, so it is left for a follow-up.

Disclosed status delta kept, not changed: UNKNOWN_TOKEN stays 400 here while Python answers 402; aligning statuses felt like a separate decision from fixing the shape.

Verification

  • TDD: new test/error_envelope.test.ts (6 tests, one per code family) written first and failing 0/6 against the old shape, green after the change.
  • Full Node suite: 125/125 (npm test), tsc clean. Two existing tests asserting the old flat shape (validation_flow.test.ts, fulfillment.test.ts) now assert the envelope code and content, which strengthens them.
  • Wire verified on a booted server: both reproductions above now answer the envelope, byte shape identical to the Python server (same ucp.version, messages[].type/code/content/severity).
  • The full official conformance suite (17 modules) runs green against the fixed server with the flower shop data.
  • Pinned pre-commit clean.

Why CI did not catch it: the existing tests asserted the flat shape as the expected one (those assertions came from #138, my own earlier behavioral suite, so this PR also corrects my own tests to the spec shape), and the fork test job runs only after maintainer approval.

Note: #171 touches the neighboring idempotency hash computation in the same functions; the changes do not overlap semantically, and I am happy to rebase whichever lands second.

Checkout business and protocol failures returned a flat {detail} JSON
body. The 2026-04-08 spec requires the UCP error envelope for business
failures (checkout.md: the response contains ucp.status "error" with
messages describing the failure) and code plus content in the body for
protocol errors (checkout-rest.md), and the Python reference already
answers this way through its typed exception taxonomy
(rest/python/server/exceptions.py + the ucp_exception_handler).

Add the same taxonomy for Node (src/utils/ucp_error.ts) and convert
every checkout error site to it, mirroring the Python codes:
IDEMPOTENCY_CONFLICT 409, RESOURCE_NOT_FOUND 404,
CHECKOUT_NOT_MODIFIABLE 409, OUT_OF_STOCK 400 (409 at complete,
matching the Python complete path), INVALID_REQUEST 400, and the
payment family INSUFFICIENT_FUNDS 402 / FRAUD_DETECTED 403 /
UNKNOWN_TOKEN with severity requires_buyer_input. HTTP statuses are
preserved at every site; this is a response shape change only.

validateInventory now throws the typed OutOfStockError so the
create and update catch blocks can render it distinctly from other
validation failures.

Left as is, matching the Python reference which answers the same way:
the testing simulation endpoints, the signature 401 wrapper, and the
internal 500 in completeCheckout.
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:needs-triage Signal that the PR is ready for human triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants