Skip to content

Commit 5d59b00

Browse files
committed
fix logic so we don't misidentify access-forbidden 403s as rate-limits
1 parent c83ee21 commit 5d59b00

File tree

2 files changed

+51
-59
lines changed

2 files changed

+51
-59
lines changed

src/utils/requests.jl

Lines changed: 50 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -120,72 +120,64 @@ function github_retry_decision(method::String, resp::Union{HTTP.Response, Nothin
120120
end
121121

122122
# At this point we have a response with status >= 400
123+
# First let us see if we want to retry it.
123124

124-
# Handle GitHub rate limiting (403, 429) with special logic
125-
if status in (403, 429)
126-
# Get all rate limit headers
127-
limit = HTTP.header(resp, "x-ratelimit-limit", "")
128-
remaining = HTTP.header(resp, "x-ratelimit-remaining", "")
129-
used = HTTP.header(resp, "x-ratelimit-used", "")
130-
reset_time = HTTP.header(resp, "x-ratelimit-reset", "")
131-
resource = HTTP.header(resp, "x-ratelimit-resource", "")
132-
retry_after = HTTP.header(resp, "retry-after", "")
133-
134-
# Check response body for secondary rate limit indicators
135-
# Note: `String` takes ownership / removes the body, so we make a copy
136-
body = String(copy(resp.body))
137-
is_secondary_rate_limit = occursin("secondary rate limit", lowercase(body)) || !isempty(retry_after)
138-
if is_secondary_rate_limit
139-
# Secondary rate limit handling
140-
# If retry-after header is present, respect it
141-
delay_seconds = safe_tryparse(Float64, retry_after)
142-
if delay_seconds !== nothing
143-
delay_seconds = parse(Float64, retry_after)
144-
verbose && @info "GitHub API secondary rate limit hit, retrying in $(round(delay_seconds, digits=1))s" method=method status=status limit=limit remaining=remaining used=used reset=reset_time resource=resource retry_after=retry_after
145-
return (true, delay_seconds)
146-
end
125+
# Note: `String` takes ownership / removes the body, so we make a copy
126+
body = String(copy(resp.body))
127+
is_primary_rate_limit = occursin("primary rate limit", lowercase(body)) && status in (403, 429)
128+
is_secondary_rate_limit = occursin("secondary rate limit", lowercase(body)) && status in (403, 429)
147129

148-
# If x-ratelimit-remaining is 0, wait until reset time
149-
reset_timestamp = safe_tryparse(Float64, reset_time)
150-
if remaining == "0" && reset_timestamp !== nothing
151-
current_time = time()
152-
if reset_timestamp > current_time
153-
delay_seconds = reset_timestamp - current_time + 1.0
154-
verbose && @info "GitHub API secondary rate limit hit, retrying in $(round(delay_seconds, digits=1))s" method=method status=status limit=limit remaining=remaining used=used reset=reset_time resource=resource retry_after=retry_after
155-
return (true, delay_seconds)
156-
end
157-
end
130+
do_retry = HTTP.RetryRequest.isidempotent(method) && (is_primary_rate_limit || is_secondary_rate_limit || HTTP.RetryRequest.retryable(status))
158131

159-
# Otherwise, wait at least 1 minute, then use exponential backoff
160-
delay_seconds = max(60.0, exponential_delay)
161-
verbose && @info "GitHub API secondary rate limit hit, retrying in $(round(delay_seconds, digits=1))s" method=method status=status limit=limit remaining=remaining used=used reset=reset_time resource=resource retry_after=retry_after
162-
return (true, delay_seconds)
163-
else
164-
# Primary rate limit handling
165-
# If x-ratelimit-remaining is 0, wait until reset time
166-
reset_timestamp = safe_tryparse(Float64, reset_time)
167-
if remaining == "0" && reset_timestamp !== nothing
168-
current_time = time()
169-
if reset_timestamp > current_time
170-
delay_seconds = reset_timestamp - current_time + 1.0
171-
verbose && @info "GitHub API primary rate limit hit, retrying in $(round(delay_seconds, digits=1))s" method=method status=status limit=limit remaining=remaining used=used reset=reset_time resource=resource
172-
return (true, delay_seconds)
173-
end
174-
end
132+
if !do_retry
133+
return (false, 0.0)
134+
end
175135

176-
# For other primary rate limit cases, use exponential backoff
177-
verbose && @info "GitHub API primary rate limit hit, retrying in $(round(exponential_delay, digits=1))s" method=method status=status limit=limit remaining=remaining used=used reset=reset_time resource=resource
178-
return (true, exponential_delay)
179-
end
136+
# Now we know we want to retry. We need to decide how long to wait.
137+
138+
# Get all rate limit headers
139+
limit = HTTP.header(resp, "x-ratelimit-limit", "")
140+
remaining = HTTP.header(resp, "x-ratelimit-remaining", "")
141+
used = HTTP.header(resp, "x-ratelimit-used", "")
142+
reset_time = HTTP.header(resp, "x-ratelimit-reset", "")
143+
resource = HTTP.header(resp, "x-ratelimit-resource", "")
144+
retry_after = HTTP.header(resp, "retry-after", "")
145+
146+
msg = if is_primary_rate_limit
147+
"GitHub API primary rate limit reached"
148+
elseif is_secondary_rate_limit
149+
"GitHub API secondary rate limit reached"
150+
else
151+
"GitHub API returned $status"
152+
end
153+
154+
# If retry-after header is present, respect it
155+
delay_seconds = safe_tryparse(Float64, retry_after)
156+
if delay_seconds !== nothing
157+
delay_seconds = parse(Float64, retry_after)
158+
verbose && @info "$msg, retrying in $(round(delay_seconds, digits=1))s" method=method status=status limit=limit remaining=remaining used=used reset=reset_time resource=resource retry_after=retry_after
159+
return (true, delay_seconds)
180160
end
181161

182-
# For other HTTP errors, check if they're retryable according to HTTP.jl
183-
if HTTP.RetryRequest.retryable(status) && HTTP.RetryRequest.isidempotent(method)
184-
verbose && @info "GitHub API HTTP error, retrying in $(round(exponential_delay, digits=1))s" method=method status=status
185-
return (true, exponential_delay)
162+
# If x-ratelimit-remaining is 0, wait until reset time
163+
reset_timestamp = safe_tryparse(Float64, reset_time)
164+
if remaining == "0" && reset_timestamp !== nothing
165+
current_time = time()
166+
if reset_timestamp > current_time
167+
delay_seconds = reset_timestamp - current_time + 1.0
168+
verbose && @info "$msg, retrying in $(round(delay_seconds, digits=1))s" method=method status=status limit=limit remaining=remaining used=used reset=reset_time resource=resource retry_after=retry_after
169+
return (true, delay_seconds)
170+
end
186171
end
187172

188-
return (false, 0.0)
173+
# If secondary rate limit hit without headers to guide us,
174+
# make sure we wait at least a minute
175+
delay_seconds = is_secondary_rate_limit ? max(60.0, exponential_delay) : exponential_delay
176+
177+
# Fall back to exponential backoff
178+
verbose && @info "$msg, retrying in $(round(delay_seconds, digits=1))s" method=method status=status
179+
180+
return (true, delay_seconds)
189181
end
190182

191183
"""

test/retries.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ using GitHub
146146
@testset "Invalid header values" begin
147147

148148
# Test with invalid retry-after header (should fall back to secondary rate limit minimum)
149-
resp1 = HTTP.Response(429, ["retry-after" => "invalid"])
149+
resp1 = HTTP.Response(429, ["retry-after" => "invalid"], Vector{UInt8}("secondary rate limit"))
150150
should_retry, sleep_seconds = GitHub.github_retry_decision("GET",resp1, nothing, 2.0; verbose=false)
151151
@test should_retry == true
152152
@test sleep_seconds == 60.0 # Falls back to secondary rate limit minimum (1 minute)

0 commit comments

Comments
 (0)