Skip to content

fix(rest/nodejs): scope idempotency hashes to checkout operations - #171

Open
XiaolongZhang-TT wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
XiaolongZhang-TT:fix/nodejs-scope-idempotency-hashes
Open

fix(rest/nodejs): scope idempotency hashes to checkout operations#171
XiaolongZhang-TT wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
XiaolongZhang-TT:fix/nodejs-scope-idempotency-hashes

Conversation

@XiaolongZhang-TT

Copy link
Copy Markdown
Contributor

Description

The Node.js sample keyed idempotency records only by the Idempotency-Key header and fingerprinted the request body alone, so the same key reused against a different checkout (or a different operation) replayed the first response instead of conflicting. This was most severe for cancel, whose hash was always the empty body, so any two cancels sharing a key were treated as identical regardless of target.

Repro: create two checkouts A and B, then cancel both with the same Idempotency-Key → the second cancel returns 200 with A's canceled checkout (silent replay) instead of conflicting.

Root causerest/nodejs/src/api/checkout.ts, computeHash(data): the fingerprint was the body only (cancel: {}), with no operation or target checkout.

Fix: include the operation and, for update/complete/cancel, the target checkout id in the fingerprint — computeHash(operation, data, resourceId?). This mirrors the Python sample (#166): create is keyed by body, update/complete/cancel additionally by the path checkout id, so a key can neither cross checkouts nor cross operations.

Category (Required)

  • Samples / Conformance: Maintaining samples and the conformance suite. (Requires Maintainer approval)

Related Issues

None — parity with the Python sample's idempotency scoping landed in #166.

Checklist

  • I have followed the Contributing Guide (Conventional Commits title requirements and ! for breaking changes).
  • I have updated the documentation (if applicable). — N/A, no documentation changes.
  • My changes pass all local linting and formatting checks. (tsc --noEmit clean; prettier clean.)
  • I have added tests that prove my fix is effective or that my feature works. (an idempotency key is scoped to the checkout (cancel) and …(update) — both fail before the fix, pass after.)
  • New and existing unit tests pass locally with my changes. (Node.js suite: 118 passed.)
  • (For Core/Capability) I have included/updated the relevant JSON schemas. — N/A, no schema changes.
  • I have regenerated Python Pydantic models by running generate_models.sh under python_sdk. — N/A, no schema changes.

Screenshots / Logs (if applicable)

Before/after — reusing one Idempotency-Key against two different checkouts:

Before (bug):

cancel A (key=k) -> 200
cancel B (key=k) -> 200   # replayed A's canceled checkout

After (fix):

cancel A (key=k) -> 200
cancel B (key=k) -> 409   # Idempotency key reused with different parameters

Full Node.js suite:

# tests 118
# pass 118
# fail 0

@damaz91 damaz91 added status:needs-triage Signal that the PR is ready for human triage status:under-review and removed status:needs-triage Signal that the PR is ready for human triage labels Aug 7, 2026

@damaz91 damaz91 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes look good and correctly scope the idempotency keys to the operation and resource ID, preventing collisions. The new tests verify this behavior.

Minor suggestion for consistency with the Python sample:
In the Python sample, resource_id is always included in the hash payload (defaulting to None/null). In the Node.js implementation, it is omitted when undefined.

To make them match closer and simplify the code, you could define payload in computeHash as:

    const payload = {
      operation,
      resourceId: resourceId ?? null,
      data,
    };

This avoids the ternary and ensures resourceId is always present (as null if not provided), mirroring the Python structure.

The Node.js sample keyed idempotency records only by the Idempotency-Key
header and fingerprinted the request body alone, so the same key reused
against a different checkout (or a different operation) replayed the first
response instead of conflicting. This was most severe for cancel, whose
hash was always the empty body, so any two cancels sharing a key were
treated as identical regardless of target.

Include the operation and, for update/complete/cancel, the target checkout
id in the fingerprint — mirroring the Python sample (Universal-Commerce-Protocol#166). Add regression
tests showing a shared key across two checkouts (cancel and update) now
returns 409 instead of replaying.
@XiaolongZhang-TT

Copy link
Copy Markdown
Contributor Author

Thanks @damaz91! Applied the suggestion in computeHashresourceId is now always present (null for create), matching the Python payload shape and dropping the ternary. Tests still green (118 passing).

@XiaolongZhang-TT
XiaolongZhang-TT force-pushed the fix/nodejs-scope-idempotency-hashes branch from bc39260 to 91835ee Compare August 11, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants