supporting the retry-after header for redirections as well

This commit is contained in:
HoneyryderChuck 2020-10-31 02:34:41 +00:00
parent 1ad2e9cbcf
commit 01552757a0
7 changed files with 63 additions and 20 deletions

View File

@ -5,6 +5,7 @@ require "httpx/version"
require "httpx/extensions" require "httpx/extensions"
require "httpx/errors" require "httpx/errors"
require "httpx/utils"
require "httpx/altsvc" require "httpx/altsvc"
require "httpx/callbacks" require "httpx/callbacks"
require "httpx/loggable" require "httpx/loggable"

View File

@ -59,8 +59,26 @@ module HTTPX
return ErrorResponse.new(request, error, options) return ErrorResponse.new(request, error, options)
end end
retry_after = response.headers["retry-after"]
if retry_after
# Servers send the "Retry-After" header field to indicate how long the
# user agent ought to wait before making a follow-up request.
# When sent with any 3xx (Redirection) response, Retry-After indicates
# the minimum time that the user agent is asked to wait before issuing
# the redirected request.
#
retry_after = Utils.parse_retry_after(retry_after)
log { "redirecting after #{retry_after} secs..." }
pool.after(retry_after) do
connection = find_connection(retry_request, connections, options) connection = find_connection(retry_request, connections, options)
connection.send(retry_request) connection.send(retry_request)
end
else
connection = find_connection(retry_request, connections, options)
connection.send(retry_request)
end
nil nil
end end

View File

@ -35,21 +35,12 @@ module HTTPX
# the minimum time that the user agent is asked to wait before issuing # the minimum time that the user agent is asked to wait before issuing
# the redirected request. # the redirected request.
# #
# The value of this field can be either an HTTP-date or a number of def retry_after_rate_limit(_, response)
# seconds to delay after the response is received.
def retry_after_rate_limit(_request, response)
retry_after = response.headers["retry-after"] retry_after = response.headers["retry-after"]
return unless retry_after return unless retry_after
begin Utils.parse_retry_after(retry_after)
# first: bet on it being an integer
Integer(retry_after)
rescue ArgumentError
# Then it's a datetime
time = Time.httpdate(retry_after)
time - Time.now
end
end end
end end
end end

18
lib/httpx/utils.rb Normal file
View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module HTTPX
module Utils
module_function
# The value of this field can be either an HTTP-date or a number of
# seconds to delay after the response is received.
def parse_retry_after(retry_after)
# first: bet on it being an integer
Integer(retry_after)
rescue ArgumentError
# Then it's a datetime
time = Time.httpdate(retry_after)
time - Time.now
end
end
end

View File

@ -36,6 +36,19 @@ module Requests
verify_status(response, 302) verify_status(response, 302)
end end
def test_plugin_follow_redirects_retry_after
session = HTTPX.plugin(SessionWithMockResponse[302, "retry-after" => "2"]).plugin(:follow_redirects)
before_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
response = session.get(max_redirect_uri(1))
after_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
verify_status(response, 200)
total_time = after_time - before_time
assert total_time >= 2, "request didn't take as expected to redirect (#{total_time} secs)"
end
def test_plugin_follow_insecure_no_insecure_downgrade def test_plugin_follow_insecure_no_insecure_downgrade
return unless origin.start_with?("https") return unless origin.start_with?("https")

View File

@ -41,7 +41,7 @@ module Requests
verify_rated_responses(rate_limiter_session, 429) verify_rated_responses(rate_limiter_session, 429)
total_time = after_time - before_time total_time = after_time - before_time
assert_in_delta 2, total_time, 1, "request didn't take as expected to retry (#{total_time} secs)" assert total_time >= 2, "request didn't take as expected to retry (#{total_time} secs)"
end end
def test_plugin_rate_limiter_retry_after_date def test_plugin_rate_limiter_retry_after_date
@ -57,7 +57,7 @@ module Requests
verify_rated_responses(rate_limiter_session, 429) verify_rated_responses(rate_limiter_session, 429)
total_time = after_time - before_time total_time = after_time - before_time
assert_in_delta 3, total_time, 1, "request didn't take as expected to retry (#{total_time} secs)" assert total_time >= 2, "request didn't take as expected to retry (#{total_time} secs)"
end end
private private

View File

@ -7,6 +7,10 @@ module SessionWithMockResponse
self self
end end
module ResponseMethods
attr_writer :status
end
module InstanceMethods module InstanceMethods
def initialize(*) def initialize(*)
super super
@ -19,11 +23,9 @@ module SessionWithMockResponse
response.close response.close
@mock_responses_counter -= 1 @mock_responses_counter -= 1
mock_response = @options.response_class.new(request, response.status = Thread.current[:httpx_mock_response_status]
Thread.current[:httpx_mock_response_status], response.merge_headers(Thread.current[:httpx_mock_response_headers])
"2.0", super(request, response)
Thread.current[:httpx_mock_response_headers])
super(request, mock_response)
end end
end end
end end