33require "net/http"
44require "uri"
55require "json"
6+ require "time"
67
78require_relative "errors"
89
@@ -73,7 +74,7 @@ def request(method, path, body: nil, params: {})
7374 response = perform ( method , uri , body )
7475 handle_response ( response , method : method , uri : uri )
7576 rescue *RETRYABLE_EXCEPTIONS => e
76- raise RequestError . new ( "request to #{ uri } failed: #{ e . message } " , cause : e ) if attempt >= @max_retries
77+ raise RequestError , "request to #{ uri } failed: #{ e . message } " if attempt >= @max_retries
7778
7879 attempt += 1
7980 wait_before_retry ( attempt )
@@ -82,7 +83,7 @@ def request(method, path, body: nil, params: {})
8283 raise e . api_error if attempt >= @max_retries
8384
8485 attempt += 1
85- wait_before_retry ( attempt )
86+ wait_before_retry ( attempt , override : e . retry_after )
8687 retry
8788 end
8889 end
@@ -126,12 +127,32 @@ def build_request(method, uri, body)
126127
127128 def handle_response ( response , method :, uri :)
128129 status = response . code . to_i
129- raise RetryableAPIError , build_api_error ( response , status , method , uri ) if status == 429
130+ if status == 429
131+ raise RetryableAPIError . new ( build_api_error ( response , status , method , uri ) ,
132+ retry_after : parse_retry_after ( response [ "Retry-After" ] ) )
133+ end
130134 raise build_api_error ( response , status , method , uri ) unless ( 200 ..299 ) . cover? ( status )
131135
132136 parse_body ( response . body )
133137 end
134138
139+ # Retry-After is usually an integer/float number of seconds, but per
140+ # RFC 9110 it may also be an HTTP-date.
141+ def parse_retry_after ( value )
142+ return nil if value . nil? || value . empty?
143+
144+ begin
145+ Float ( value )
146+ rescue ArgumentError , TypeError
147+ begin
148+ seconds = Time . httpdate ( value ) - Time . now
149+ seconds . positive? ? seconds : nil
150+ rescue ArgumentError
151+ nil
152+ end
153+ end
154+ end
155+
135156 def build_api_error ( response , status , method , uri )
136157 data = parse_body ( response . body )
137158 message = data . is_a? ( Hash ) ? ( data [ "message" ] || data [ "error" ] || response . body ) : response . body
@@ -153,19 +174,27 @@ def parse_body(raw)
153174 raw
154175 end
155176
156- def wait_before_retry ( attempt )
177+ def wait_before_retry ( attempt , override : nil )
178+ if override
179+ @sleeper . call ( override )
180+ return
181+ end
182+
157183 base = self . class . backoff_delay ( attempt - 1 , initial_delay : @initial_delay , backoff_factor : @backoff_factor )
158184 jitter = base * JITTER_RATIO * ( ( rand * 2 ) - 1 )
159185 @sleeper . call ( base + jitter )
160186 end
161187
162188 # Internal-only signal so 429s share the same retry path as network
163- # errors without retrying every other 4xx/5xx status.
189+ # errors without retrying every other 4xx/5xx status. Carries the
190+ # server's Retry-After hint (if any) so the wait honors it instead of
191+ # our own computed backoff.
164192 class RetryableAPIError < StandardError
165- attr_reader :api_error
193+ attr_reader :api_error , :retry_after
166194
167- def initialize ( api_error )
195+ def initialize ( api_error , retry_after : nil )
168196 @api_error = api_error
197+ @retry_after = retry_after
169198 super ( api_error . message )
170199 end
171200 end
0 commit comments