fix: never retry non-idempotent requests after ambiguous failures - #10
Open
jonmarkgo wants to merge 1 commit into
Open
fix: never retry non-idempotent requests after ambiguous failures#10jonmarkgo wants to merge 1 commit into
jonmarkgo wants to merge 1 commit into
Conversation
A network error or a server 5xx leaves it unknown whether the server processed the request. The retry loop replayed both regardless of HTTP method, so a POST that timed out after Forem had already created a resource would create a second one. Retries are now gated on idempotency: - APIConnectionError and 5xx are replayed only for idempotent methods (GET, HEAD, PUT, DELETE, OPTIONS, TRACE). - 429 is still retried for every method, including POST. The server rejected the request without processing it, so replaying it cannot duplicate anything. - Callers whose POST endpoint deduplicates server-side can opt back in per request with `opts[:idempotent] = true`. No configuration change is required and idempotent requests behave exactly as before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
APIRequestor#requestretries transient failures without considering the HTTP method.retryable_error?branches only on error class and status:A network error or a 5xx leaves it unknown whether the server processed the request. Replaying a
POSTin that state can create a second resource — soclient.articles.create(...)can silently publish a duplicate article under a user's name when a request times out after Forem accepted it.max_network_retriesdefaults to1, so this is on by default for every consumer.Change
Retries are now gated on idempotency:
APIConnectionErrorand 5xx are replayed only for idempotent methods —GET,HEAD,PUT,DELETE,OPTIONS,TRACE(RFC 9110). APOSTraises to the caller instead.POST. A rate-limited request was rejected without being processed, so replaying it cannot duplicate anything. This distinction is the reason the fix isn't simply "don't retry POST" — it keeps the genuinely useful rate-limit behaviour intact.POSTendpoint deduplicates server-side (idempotency key, natural key) can opt back in per request withopts[:idempotent] = true.No configuration change is required, and idempotent requests behave exactly as before.
Tests
Six new tests in
test/forem/api_requestor_test.rb, using acounting_requestorhelper that reports how many attempts actually reached the transport:POSTis not retried after a 5xxPOSTis not retried after a connection errorPUTis still retried after a 5xxPOSTis retried after a 429POSTis retried when explicitly marked idempotent:idempotentoption is consumed, not leaked as a request headerFull suite: 330 runs, 975 assertions, 0 failures.
I also checked the tests fail without the fix — reverting just the two guard conditions fails exactly the two POST-safety tests (
330 runs, 2 failures) and nothing else, so they're pinning the behaviour rather than passing vacuously.Why we hit this
We're building a delegated write proxy in MLH Core that forwards article creates to Forem on a user's behalf. We had to hand-roll a Faraday transport specifically to get POST-never-retry semantics, duplicating a good deal of what
APIRequestoralready does well. With this fix we can drop that and use the gem — and, more importantly, the safety property is enforced in the client for every consumer rather than re-implemented by each one.Happy to adjust the naming or the opt-in mechanism if you'd prefer a different shape.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.