LearnNewsExamplesServices
Frontmatter
id15454
titleGraphQL query() 5xx and in-body-gateway retries replay non-idempotent mutations
stateClosed
labels
bugai
assigneesneo-opus-ada, neo-opus-grace
createdAtJul 18, 2026, 12:24 PM
updatedAtJul 18, 2026, 1:32 PM
githubUrlhttps://github.com/neomjs/neo/issues/15454
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 18, 2026, 1:32 PM

GraphQL query() 5xx and in-body-gateway retries replay non-idempotent mutations

neo-opus-ada
neo-opus-ada commented on Jul 18, 2026, 12:24 PM

Context

Surfaced during the #15359 / PR #15419 review — @neo-gpt's RC1 established that DevIndex.services.GitHub.query() (apps/devindex/services/GitHub.mjs) must not replay a non-idempotent GraphQL mutation after an ambiguous transport outcome (the write may have applied before the failure). PR #15419 fixed exactly that at the transport-catch retry, gating mutations via a new #isMutation() retry-authorization check.

The Problem

Two OTHER retry paths in the same query() share the ambiguity but were out of PR #15419's shared-classification scope — they are a separate, pre-existing retry mechanism (old-style (4 - retries) * 2000 delay, not the shared #isRetryableTransientError classifier):

  • 5xx / 403 retry (GitHub.mjs ~line 336): a >= 500 server error leaves a mutation's server-side outcome ambiguous (it may have applied before the error), yet it is retried. A 403 is a pre-execution rate-limit rejection — safe to replay.
  • In-body gateway 502/504 retry (GitHub.mjs ~line 372): a gateway error is likewise ambiguous for a mutation.

OptIn/OptOut call query() with addComment + issue-close mutations at retries: 3, so a 5xx / gateway blip mid-write can duplicate the comment or re-close the issue.

The Fix (shape)

Extend the retry-authorization gate (#isMutation(), shipped in PR #15419) to these two ambiguous paths: a mutation is not replayed on a >= 500 server error or an in-body 502/504; a 403 rate-limit and a 200-body rejection stay retryable. Alternatively, separate mutation execution from the read-retrying query() entirely so retry classification and retry authorization are distinct surfaces. The reviewer's own RETROSPECTIVE captures it: "a failure may be transient while replay is still unsafe."

Acceptance Criteria

  • A mutation is not replayed after a >= 500 server error (the :336 path); a 403 rate-limit still retries.
  • A mutation is not replayed after an in-body 502/504 gateway error (the :372 path).
  • An idempotent read (query) still retries all of the above.
  • A discriminating witness (mutation-not-replayed / read-retried) per gated path, red against the unfixed code — mirroring the transport-path witness shipped in PR #15419.

Out of Scope

  • The transport-catch path (fixed in PR #15419).
  • REST-path retries (idempotent GETs).

Retrieval Hint: "GraphQL query 5xx gateway mutation replay idempotent retry authorization devindex GitHub"