Conversation
Three defects, one subject: how long a call can actually take. `timeout` only set open_timeout and read_timeout. Net::HTTP's write timeout kept its own default, 60 seconds in the runtime here, so a request whose body stalled on the way out ran twelve times longer than `timeout: 5` promised, and past the 30s `total_timeout` as well. The TLS handshake had no limit of its own either. Set all four. Net::WriteTimeout was not in TIMEOUT_EXCEPTIONS, so the one failure the missing write timeout produces was classified as a plain transport error and never retried, whatever `retry_timeouts` said. `total_timeout` was documented as a budget "across attempts and delays" but was only ever consulted around the sleeps. An attempt already in flight ran to its own `timeout` regardless, so the real worst case was the budget plus a full attempt. Each attempt now gets the smaller of `timeout` and the remaining budget, and an attempt that would start with nothing left raises TimeoutError instead of running. The comparison is `>=` now: a delay that lands exactly on the deadline has spent the whole budget, and the attempt after it would have had zero seconds. Handing an attempt its budget needs a way to tell the transport. The contract grows an optional `timeout:` keyword, and a transport that does not declare one is called exactly as it was before, so existing custom transports keep working. `timeout:` itself was unvalidated, so `timeout: "5"` failed inside Net::HTTP on the first request rather than when the client was built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Three defects, one subject: how long a call can actually take.
timeoutdidn't bound the request. Onlyopen_timeoutandread_timeoutwere set.Net::HTTP's write timeout kept its own default — 60 seconds in the runtimes I checked — so a request whose body stalled on the way out ran twelve times longer thantimeout: 5promised, and past the 30stotal_timeoutas well. The TLS handshake had no limit of its own either.Net::WriteTimeoutwas not inTIMEOUT_EXCEPTIONS, so the one failure that gap produces was classified as a plainTransportErrorand never retried, whateverretry_timeoutssaid. (It's aTimeout::Error<RuntimeError, so it was caught by the genericrescue StandardErrorand passed straight through.)total_timeoutwas documented as a budget "across attempts and delays" but was only consulted around the sleeps. An attempt already in flight ran to its owntimeoutregardless, so the real worst case was the budget plus a full attempt.The fix
Net::HTTPtimeouts: open, ssl, read, write.Net::WriteTimeouttoTIMEOUT_EXCEPTIONS.timeoutand the remaining budget, so one slow attempt cannot outlive the whole call. An attempt that would start with nothing left raisesTimeoutErrorinstead of running.budget_exceeded?compares with>=: a delay landing exactly on the deadline has spent the whole budget, and the attempt after it would have had zero seconds.timeout:at construction —timeout: "5"used to fail insideNet::HTTPon the first request.Handing an attempt its budget needs a way to tell the transport, so the contract grows an optional
timeout:keyword. A transport that doesn't declare one is called exactly as before, so existing custom transports keep working — there's a test for that.Tests
test/deadline_test.rb, 14 cases: budget clamping across attempts, the exact-deadline boundary, write-timeout retry and opt-out, backwards compatibility of the three-keyword transport, and a stubbed check that all fourNet::HTTPtimeouts are set. Suite green on 3.2.11 / 3.3.8 / 3.4.8.One of a series from a security and API-coverage audit. Branches are independent, each off
main.🤖 Generated with Claude Code