Skip to content

Commit 0d65a77

Browse files
hsbtclaude
andcommitted
Retry without Range when compact index returns 416
Bundler's downloader already recovers from a stale over-long cache by dropping the Range header and refetching the whole file, but Gem::CompactIndexClient::HTTPFetcher raised FetchError instead, so the gem CLI could not self-heal. Align the behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent dbd2465 commit 0d65a77

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

lib/rubygems/compact_index_client/http_fetcher.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ def fetch(uri, headers, redirects_remaining)
4141
raise Gem::RemoteFetcher::FetchError.new("redirecting but no redirect location was given", uri) unless location
4242

4343
fetch(uri + location, headers, redirects_remaining - 1)
44+
when Gem::Net::HTTPRangeNotSatisfiable
45+
raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri) unless headers.key?("Range")
46+
47+
# The local cache is longer than the remote file, refetch it whole.
48+
fetch(uri, headers.except("Range"), redirects_remaining)
4449
else
4550
raise Gem::RemoteFetcher::FetchError.new("bad response #{response.message} #{response.code}", uri)
4651
end

test/rubygems/test_gem_compact_index_client_http_fetcher.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ def initialize
3434
end
3535
end
3636

37+
class FakeRangeNotSatisfiable < Gem::Net::HTTPRangeNotSatisfiable
38+
def initialize
39+
super("1.1", "416", "Range Not Satisfiable")
40+
end
41+
end
42+
3743
class FakeRemoteFetcher
3844
attr_reader :requests
3945

@@ -116,6 +122,35 @@ def test_call_raises_after_too_many_redirects
116122
assert_match(/too many redirects/, error.message)
117123
end
118124

125+
def test_call_retries_without_range_on_range_not_satisfiable
126+
requests = []
127+
remote = Object.new
128+
remote.define_singleton_method(:request) do |uri, request_class, &block|
129+
request = request_class.new(uri)
130+
block&.call(request)
131+
requests << request
132+
request["Range"] ? FakeRangeNotSatisfiable.new : FakeResponse.new("full data")
133+
end
134+
135+
fetcher = Gem::CompactIndexClient::HTTPFetcher.new("https://index.example", remote)
136+
response = fetcher.call("versions", "Range" => "bytes=100-", "If-None-Match" => '"abc"')
137+
138+
assert_equal "full data", response.body
139+
assert_equal 2, requests.size
140+
assert_nil requests.last["Range"]
141+
assert_equal '"abc"', requests.last["If-None-Match"]
142+
end
143+
144+
def test_call_raises_on_range_not_satisfiable_without_range
145+
fetcher, _remote = fetcher_for("https://index.example/versions" => FakeRangeNotSatisfiable.new)
146+
147+
error = assert_raise Gem::RemoteFetcher::FetchError do
148+
fetcher.call("versions")
149+
end
150+
151+
assert_match(/bad response Range Not Satisfiable 416/, error.message)
152+
end
153+
119154
def test_call_raises_fetch_error_on_failure_response
120155
fetcher, _remote = fetcher_for("https://index.example/versions" => FakeNotFound.new)
121156

0 commit comments

Comments
 (0)