Objective: Improve src/routes/api/v1/postage/$messageId/settle.ts by addressing reliability gaps in settlement retries.
Problem: Settlement retries during network failures could double-resolve escrow state or produce conflicting responses.
Solution: Implement idempotency semantics with request correlation handling.
Implementation:
- Added
X-Idempotency-Keyheader support to settlement endpoint - Idempotency keys are hashed with SHA-256 and scoped per recipient (actor isolation)
- Cached responses (both success and terminal errors) are replayed for repeated calls
- Terminal state transitions (pending → settled, pending → refunded) produce consistent 409 errors
Evidence:
// From settle.ts
const rawIdempotencyKey = request.headers.get("x-idempotency-key");
if (rawIdempotencyKey) {
const existing = await checkIdempotency(repository, current.recipient, rawIdempotencyKey);
if (existing) {
return apiSuccess(request, existing.body, {
status: existing.status,
headers: { "x-idempotency-replayed": "true" },
});
}
}Test Coverage (from tests/unit/api/postage-settlement-idempotency.test.ts):
-
Retry after success:
"handles retry after successful settlement (same idempotency key)"- Verifies replayed response matches original
- Confirms no double-settlement occurs
-
Retry after terminal state:
"returns deterministic error when settling already-settled postage""returns deterministic error when settling already-refunded postage""handles retry after terminal-state error (same idempotency key)"
-
Additional coverage:
- Actor isolation tests
- Network failure retry scenarios
- Multiple operations with different keys
- Data integrity across retries
- Edge cases (missing postage, etc.)
Test Results: All 660 tests passing (including 17 new settlement idempotency tests)
Implementation (from postage-service.ts):
if (postage.status !== "pending") {
const explanations: Record<string, string> = {
settled:
"Postage has already been settled. The escrow was previously released to the recipient.",
refunded:
"Postage has already been refunded. The escrow was previously returned to the sender.",
};
throw new ApiError(409, "conflict", explanation, {
currentStatus: postage.status,
attemptedStatus: status,
messageId,
});
}Example Response:
{
"error": {
"code": "conflict",
"message": "Postage has already been settled. The escrow was previously released to the recipient.",
"details": {
"currentStatus": "settled",
"attemptedStatus": "settled",
"messageId": "abc123..."
}
}
}-
src/routes/api/v1/postage/$messageId/settle.ts- Added idempotency key handling
- Implemented response caching and replay
- Added comprehensive documentation
-
src/server/api/postage-service.ts- Enhanced
resolvePostagewith detailed error messages - Improved terminal state explanations
- Enhanced
-
protocol/vectors/vectors.json- Updated test vector to match new error message
-
tests/unit/api/postage-settlement-idempotency.test.ts- 17 comprehensive test cases
- Covers all retry scenarios
- Validates actor isolation and security
-
docs/api/SETTLEMENT_IDEMPOTENCY.md- Complete idempotency documentation
- Request flow diagrams
- Client best practices
- Security considerations
-
docs/api/README.md(updated)- Added idempotency section
- References to detailed documentation
✅ Build completed successfully with no errors
✅ All 660 unit tests passing (59 test files)
- Existing postage service tests: ✅ Pass
- New idempotency tests: ✅ Pass
- Protocol vector tests: ✅ Pass (updated for new error message)
| Scenario | Status | Test Name |
|---|---|---|
| Settle pending postage | ✅ Pass | resolvePostage - deterministic terminal states |
| Retry settled postage | ✅ Pass | retry-after-success |
| Retry refunded postage | ✅ Pass | retry-after-terminal-state |
| Actor isolation | ✅ Pass | actor isolation tests |
| Network failure retries | ✅ Pass | network failure scenarios |
| Multiple operations | ✅ Pass | different keys for different operations |
| Data integrity | ✅ Pass | preserves postage data across retries |
-
f607e2c8 -
feat: add idempotency support to postage settlement endpoint- Core implementation
- Test suite
- Error message improvements
-
c4700e40 -
docs: add comprehensive settlement idempotency documentation- Complete documentation
- Request flow diagrams
- Client examples
- Actor Isolation: Keys are scoped per recipient using
hash(actor:key) - Key Hashing: SHA-256 prevents key leakage in logs
- No Cross-Actor Replay: Different recipients cannot replay each other's responses
- Terminal Error Caching: Only 409 conflicts cached, not transient 500 errors
- Idempotency check: O(1) cache lookup
- Recording: O(1) cache write
- Without key: No performance impact
- Storage: Same KV repository as postage state
✅ Fully backward compatible:
X-Idempotency-Keyheader is optional- Existing clients without the header work unchanged
- No breaking API changes
Potential improvements for future iterations:
- TTL on cached idempotency records (e.g., 24 hours)
- Metrics for idempotency replay rates
- Support for refund endpoint idempotency
- OpenAPI spec updates
All acceptance criteria have been met:
- ✅ Deterministic settlement behavior
- ✅ Comprehensive test coverage for retry scenarios
- ✅ Clear, actionable error messages for terminal states
- ✅ Build and tests pass
- ✅ 2 commits made during implementation
The settlement endpoint now provides production-grade idempotency support, preventing double-settlement and ensuring safe retry behavior during network failures.