Repository navigation
Description
Verified against (commit SHA or released version)
commit 988c035 (v0.14.0)
Discord Username / User ID
uisupersaiyan3
What does this improvement do?
#103 / PR #256 stopped transport retries from replaying writes that carry no Idempotency-Key (canRetryTransport, http.ts:1505-1507, applied at :817). PR #256 explicitly "leaves API-envelope retry behavior unchanged", and that path has the same hole: the API-error branch calls apiRetryDecision (http.ts:1076-1086) without checking allowTransportRetry, so INTERNAL is retried once and UNAVAILABLE up to 4 attempts (:1565-1576) for any method, keyed or not.
It also defeats the 504 case #103 targeted: the edge gate only fires when rawBody === null (:1010). An API-Gateway-style 504 with body {"message":"Endpoint request timed out"} parses as JSON, codeFromHttpStatus maps it to INTERNAL (errors.ts:469-499), and the write is replayed. An envelope-less 503 maps to UNAVAILABLE and is replayed too.
Affected keyless writes: mintTunnel (http.ts:616-625, used by test run --local, loopback environments, tunnel start), cancelRun (:686), and every generic post/put/patch/delete (:359-371: project create, test create, etc.). A commit-then-5xx response mints a second tunnel binding: the CLI only knows and deletes the second one, so the first stays a live inbound credential until its TTL (2h default) and counts against the small per-principal binding cap, which can fail the next --local run with tunnel_binding_limit. For project/test create it is the duplicate-resource case #103 described.
Details / implementation notes
Repro (vitest, real HttpClient.mintTunnel, first response is the case under test, second is a 200 mint body):
[probe] bare 504 html (gated): POSTs=1 (PR #256 gate works)
[probe] API-Gateway style 504 JSON: POSTs=2 idempotencyKeySent=false -> minted c-1
[probe] 500 envelope (INTERNAL): POSTs=2 idempotencyKeySent=false -> minted c-1
[probe] 503 envelope-less: POSTs=2 idempotencyKeySent=false -> minted c-1
Why tests miss it: the #256 regression tests (http.test.ts:738, :834) cover thrown transport errors and a bare edge response only; no test sends a keyless write a 500 / 503 / JSON 504.
Proposed fix (inside requestWithMeta, no new dependency):
- When
!allowTransportRetry(keyless non-GET/HEAD), do not retryINTERNALorUNAVAILABLE. KeepRATE_LIMITEDretries: a 429 means the request was not processed. - Treat 408/502/504 as transport-edge whenever the body is not a catalog
{error:{code}}envelope, not only when it is empty.
Tests: keyless POST + 500 / 503 / JSON 504 makes exactly 1 call and surfaces the error; keyed POST is retried as today; GET + JSON 504 still gets transport retries.
Not a duplicate: #103 / PR #256 are transport-only by their own description; PR #333 (closed unmerged) did not land; #422 / #431 are body-read resets on safe retries; #437 is poll-layer outage handling. PR to follow once assigned.
Confirmations
- I have searched existing issues and this is not a duplicate.
- I have provided my Discord identity above for reward coordination.
Activity
fyZhang66 commentedon Oct 20, 2026
Confirmed on v0.15.0, thanks @crypticsaiyan. The API-error (5xx envelope) retry branch in
http.tsis gated onallowRetry, not on the transport rule from #256, so a keyless write (tunnel mint) is replayed; a JSON-bodied 504 also misses the empty-body edge gate and falls through asINTERNAL. Accepted, scope:Idempotency-Keyon a write → no retry" rule in the API-error branch too, and treat a 502/503/504 with a JSON body like the empty-body case.Idempotency-Keyto tunnel mint: that endpoint is deliberately non-idempotent on the server (its response carries a secret) and is rate-limited instead, so the key would be ignored. Not retrying it is the fix.No new dependency. Comment
/assignto claim it.crypticsaiyan commentedon Oct 21, 2026
/assign
testsprite-hob commentedon Oct 21, 2026
Assigned to @crypticsaiyan. Thanks for taking this on. (2 more slots available.)
Your open assigned issues (1/3):
(none)
fix(http): do not replay keyless writes on 5xx API errors (TestSprite…