fix(rest/nodejs): scope idempotency hashes to checkout operations - #171
Conversation
damaz91
left a comment
There was a problem hiding this comment.
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.
|
Thanks @damaz91! Applied the suggestion in |
bc39260 to
91835ee
Compare
Description
The Node.js sample keyed idempotency records only by the
Idempotency-Keyheader 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 forcancel, 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 cause —
rest/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):createis keyed by body,update/complete/canceladditionally by the path checkout id, so a key can neither cross checkouts nor cross operations.Category (Required)
Related Issues
None — parity with the Python sample's idempotency scoping landed in #166.
Checklist
!for breaking changes).tsc --noEmitclean;prettierclean.)an idempotency key is scoped to the checkout (cancel)and…(update)— both fail before the fix, pass after.)Screenshots / Logs (if applicable)
Before/after — reusing one
Idempotency-Keyagainst two different checkouts:Before (bug):
After (fix):
Full Node.js suite: