Idempotency Middleware Body Integrity (empty/synthetic body substitution)
On this page
Status
| Unit | Description | Status |
|---|---|---|
U0 |
Choreography: label, issue amendments (#1183 v2 / #1182 scope expansion), #1194 filed, this plan |
Done (2026-07-28) — commit f2407513 |
U1 |
Fix (terminal rule, frame capture, drop-guard, typed finalize outcomes) + 17-scenario matrix + proptest + RED-proofs |
Done (2026-07-28) — commit b00eacf2; RED-proof 13/17 integration failures against the pre-fix binary (the 4 passes are the designed pre-fix-green pins); all 17 green post-fix + capture proptest + cache/response unit tests; J-review PASS with 2 doc-honesty flags remediated pre-commit (terminal-rule overclaim reworded; two panic-before-teardown escapes collected) |
U2 |
Docs: ADR-022 §D5 refresh + amendment, data-model-security, CHANGELOG |
Done (2026-07-28) — stale pre-atomic-claim DDL refreshed; the unsafe "client re-executes" instruction removed at both cites; mixed-fleet + no-repair honesty in CHANGELOG |
Ship |
Battery, MR, merge, close-out |
Done (2026-07-28) — MR !1096 merged (48353f49); battery attempt 2 green (attempt 1 fast-failed at the fn-name-and cheap gate — four renames, b9c7e843); #1183 closed with AC walk; epic &74 ticked |
Epic: &74
Issue: #1183 (Plan::IDEM-BODY)
Follow-ups: #1194 (expiry race — filed by this plan’s U0); #1182 (scope EXPANDED by U0: generation fencing, fingerprint-checked recovery, sentinel fidelity, client recovery policy — still gated on the maintainer’s contract steer)
Review state: v1 internal (Explore + Plan subagent passes, version-pinned API verification); v2 folded the 2026-07-28 external review (~27 findings — terminal rule, cap-split, trailers, cancellation, typed outcomes, expiry-race correction, wire/metrics honesty, test-matrix + routing corrections). Steer 2026-07-28: standalone v2; fencing → expanded #1182. Approved 2026-07-28.
Context
run_handler_and_finalize (crates/craig-api/src/idempotency/middleware.rs:301-365), mounted once (crates/craig-api/src/lib.rs:373-376) and inherited by all 8 ApiServer services (authed POSTs carrying Idempotency-Key; no first-party client sends it — exposure is contract-following SDK/external clients):
-
(a) Oversized 2xx poisons the cache.
to_bytes(resp_body, RESPONSE_BODY_LIMIT + 1)errs for any body > limit+1;Err(_) ⇒ Bytes::new()feeds EMPTY bytes tofinalize_succeeded(cache.rs:270-307) which computestoo_large = falseand persists a canonicalsucceededrow with an empty body — 2xx/empty live AND on every replay until expiry+cleanup. The documented sentinel (ADR-022 §D5) only materializes at EXACTLY limit+1 bytes.RESPONSE_BODY_LIMIT= 1 MiB (cache.rs:30). -
(b) Finalize DB error swaps in an empty body.
finalize_succeededconsumes the bytes;unwrap_or_else(|_| Bytes::new())(middleware.rs:351-354) discards them on Err while the log claims "returning live response". -
(c) Non-2xx arm truncates for nothing. Same capped
to_bytes+ empty-on-err, butfinalize_failedstores no body. Both arms rebuild via a builder loop that drops response extensions + version. -
(d) The terminal rule (v2). Once
next.runreturns 2xx the handler’s side effects are COMMITTED: any later delivery failure (over-cap tail error, mid-stream body error, client cancellation) must finalize into a NON-reclaimable success state —failedwould let Path-D re-claim and re-execute the POST. The existingsucceeded + body_too_large=truesentinel IS that state ("completed, response unavailable on replay") — no new status, no migration; one rule resolves over-cap/mid-error/cancellation uniformly.
Design
| # | Decision | Substance |
|---|---|---|
D1 |
Post-2xx terminal rule |
After a 2xx, the only legal cache outcomes are |
D2 |
Frame-level capture, cap-split zero-copy |
|
D3 |
Arm handling |
|
D4 |
|
|
D5 |
Drop-guarded post-2xx finalization |
|
D6 |
|
Non-2xx: |
D7 |
Observability |
Structured events: |
D8 |
Wire honesty |
No schema migration. Framing MAY change for over-cap winners: explicit handler |
Units
U0 — label Plan::IDEM-BODY; #1183 v2 amendment note; #1182 scope-expansion note (generation fencing, fingerprint-checked recovery, sentinel fidelity, client recovery policy, lease-crossing pin, contention tests); #1194 filed (expiry race) + /relate #1183/#1182; this plan + nav; cargo xtask plan-lint.
U1 — code per D1–D8; proptest on the capture invariant (compact descriptors biased around cap−1/cap/cap+1, small test cap, ≈64 cases, one shared runtime; recorded as outside cargo xtask reliability’s `::properties:: selector — default sweep + MR CI --lib run it); integration target crates/craig-api/tests/idempotency_middleware.rs (#[ignore = "requires devstack"]; per-test throwaway DBs craig_idem_1183_{uuidv7.simple()} from craig_test_lib::postgres_test_db_url("postgres"); schema via sqlx::raw_sql over the two byte-identical craig-cases idempotency migrations; collect-observations → unconditional teardown → assert); harness = Router + idempotency layer + claims-inject layer (prod order), oneshot on a Router clone per request, sole-row DB asserts, AtomicUsize invocation counts; finalize faults via a scoped BEFORE UPDATE … WHEN (NEW.status='succeeded') RAISE trigger (table stays usable). 17-scenario matrix (the exact-cap replay pin folded into the exact-cap test) — (oversized-streamed RED, exact-cap pin, one-byte-over, giant single frame, finalize-failure RED, overflow-finalize-failure, oversized-non-2xx RED, body-error-before-cap RED, body-error-after-cap, different-fingerprint-422, loser-replays-while-winner-streams, cancellation RED, marker/extensions/version preservation, trailers live-only, one-connection pool, lease-crossing #1182 pin, real-TCP wire check) + response.rs/cache.rs unit tests (sentinel replay shape; defensive oversize; finalize-on-missing-row = Lost). RED-proof: git stash push — crates/craig-api/src (tests + manifests + lockfile stay), run, record, pop.
U2 — ADR-022: §D5 schema block refreshed (pre-atomic-claim DDL is stale) + the unsafe "client re-executes against the live handler" sentence REMOVED + dated #1183 amendment (terminal rule, full-body guarantee, framing deltas, sentinel-before-tail visibility, trailers live-only, finalize outcome taxonomy, metrics discontinuity, the no-pooled-connections-in-bodies constraint, mixed-fleet honesty); data-model-security.adoc cache-key (4→5-tuple) + re-execute text corrected; CHANGELOG === Fixed (schema-compat vs mixed-version behavioral safety separated; no-repair + corrected expiry rationale; framing/metrics deltas); plan Status per unit.
Verification
-
Integration:
cargo nextest run -p craig-api --test idempotency_middleware --run-ignored=all(devstack up). Joinscargo xtask validate’s integration battery. Pre-push-only coverage: MR CI runs `--lib --bins;cargo xtask reliability’s `::properties::selector matches neither this target nor the unit proptest. -
Unit/prop:
cargo nextest run -p craig-api capture_up_to_cap+cargo nextest run -p craig-api replay_of_too_large(default sweep; MR CI runs them). -
cargo clippy -p craig-api --all-targets --locked — -D warnings(unfulfilled-#[expect]trap;--lockedproves the committed lockfile). -
Full battery pre-push (no reseed — no migration).
Out of scope (routed)
-
Generation fencing, conditional finalizers, fingerprint-checked failed/expired reclaim, sentinel replay fidelity, client recovery policy, lease-crossing double-execution, stale-finalizer contention → #1182 (scope expanded by U0; maintainer-gated).
-
Expiry race → #1194.
-
Repeated-header (
set-cookie) collapse → #968. -
Poisoned-row repair → impossible to target safely; rationale in CHANGELOG.