Skip to content

[Hackathon] fix(http): 5xx API-error retries still replay keyless writes (duplicate tunnel mints, projects, tests) #490

Closed

Description

@crypticsaiyan

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 retry INTERNAL or UNAVAILABLE. Keep RATE_LIMITED retries: 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

  1. fyZhang66 commented on Oct 20, 2026

    @fyZhang66
    Contributor

    Confirmed on v0.15.0, thanks @crypticsaiyan. The API-error (5xx envelope) retry branch in http.ts is gated on allowRetry, 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 as INTERNAL. Accepted, scope:

    • Apply the "no Idempotency-Key on 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.
    • Do not add an Idempotency-Key to 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.
    • Tests (must fail before the fix): keyless POST + 5xx envelope → one request, exit 10; keyed POST + 5xx → retried; GET + 5xx → retried.

    No new dependency. Comment /assign to claim it.

  2. added
    acceptedTriaged and accepted — a PR for this will be reviewed
    and removed
    needs-triageNew issue/PR awaiting first triage
    on Oct 20, 2026
  3. crypticsaiyan commented on Oct 21, 2026

    @crypticsaiyan
    ContributorAuthor

    /assign

  4. testsprite-hob commented on Oct 21, 2026

    @testsprite-hob

    Assigned to @crypticsaiyan. Thanks for taking this on. (2 more slots available.)

    Your open assigned issues (1/3):
    (none)

  5. added a commit that references this issue on Nov 9, 2026
    e3ad901
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

acceptedTriaged and accepted — a PR for this will be reviewedhackathonCLI hackathon submissionsin-progressAssigned and actively being worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions