httpx 0.19.8 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05a379db82c9c86f84a847680f4a784ed62a4cbcb8ce8d09a3d3c29abeb7d5d1
4
- data.tar.gz: 5046f5d1e86f7d5c523a171b23fd5fb8126b63636a021c204a7c33a4ebf5e319
3
+ metadata.gz: a5a31aeffa012bd1eea19c1e892c8669624de0d21c9cd8911ae025375f0b93fb
4
+ data.tar.gz: 285707682eb2d1451106d28bc2d4a8ceb247d4237139c9e7d83bd5a0163b5a5c
5
5
  SHA512:
6
- metadata.gz: d62d606f0688c485f79a38feed6b94841005fb6a70c5ab934c7b70cb7a7958066b85a496405959a7660da3f12f194d844f10660621ce6fb98c7aa898c38d855c
7
- data.tar.gz: 42b46c44c4c864222f14235534be31db4eb5155716c542457178be010b227fdb391c095b009759e9c237d6320da7133123334cfc4b3d4dd6940ff58aa6df9f0e
6
+ metadata.gz: 6f2c50aa8895c6f07ed0acbca1b1171343da2b55bc838ae7f2a0f7a44300c101ea0020938946bf630b80ad6b81bed3cb54910b0734576b6e7c000e058497128c
7
+ data.tar.gz: 83e19a86d841056821d9198c9a8e3e7b06ec354445289a1fd7d612d070b02b0a01aa9f45f5acdb0b35e4da413c2c3955646e5f55c5421db95879092a0a3aa377
@@ -0,0 +1,36 @@
1
+ # 0.19.0
2
+
3
+ ## Features
4
+
5
+ ### Sentry integration
6
+
7
+ Documentation: https://gitlab.com/honeyryderchuck/httpx/-/wikis/Sentry-Adapter
8
+
9
+ `httpx` ships with integration for `sentry-ruby` to provide HTTP request specific breadcrumbs and tracing. It can be enabled via:
10
+
11
+ ```ruby
12
+ require "httpx/adapters/sentry"
13
+ ```
14
+
15
+ ### Proxy alternative auth schemes
16
+
17
+ Besides the already previously supported (and still default) HTTP Basic Auth, the `:proxy` plugin supports HTTP Digest and NTLM auth as well. These are made available via the following APIs:
18
+
19
+ ```ruby
20
+ http = HTTPX.plugin(:proxy)
21
+ http.with_proxy_basic_auth(username: "user", password: "pass", uri: "http://proxy-uri:8126")
22
+ http.with_proxy_digest_auth(username: "user", password: "pass", uri: "http://proxy-uri:8126")
23
+ http.with_proxy_ntlm_auth(username: "user", password: "pass", uri: "http://proxy-uri:8126")
24
+
25
+ # or alternatively
26
+ http.with_proxy(proxy: "basic", username: "user", password: "pass", uri: "http://proxy-uri:8126")
27
+ ```
28
+
29
+ ## Bugfixes
30
+
31
+ * HTTPS requests on an URL with an IP as a host, will now correctly not perform SNI during the TLS handshake, as per RFC;
32
+ * `:follow_redirects` plugin will now halt redirections on 3xx responses with no `"location"` headers; this means it won't crash on 304 responses.
33
+ * If the `httpx` session has the `:proxy` plugin enabled, HTTP 305 responses will retry the request via the proxy exposed in the `"location"` header, as the RFC mandates.
34
+ * `alt-svc` connection switch for HTTPS requests will be halted if the advertised alternative service "downgrades" to cleartext (example: `alt-svc` advertises `"h2c"`, but original connection was enabled via TLS).
35
+ * A new connection to a TLS-enabled `alt-svc` advertised for a previous request, will now use that request's hostname as the SNI hostname, when performing the TLS handshake.
36
+ * the `:response_cache` plugin will now correctly handle capitalized HTTP headers advertised in the `"vary"` header.
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry-ruby"
4
+
5
+ module HTTPX::Plugins
6
+ module Sentry
7
+ module Tracer
8
+ module_function
9
+
10
+ def call(request)
11
+ sentry_span = start_sentry_span
12
+
13
+ return unless sentry_span
14
+
15
+ set_sentry_trace_header(request, sentry_span)
16
+
17
+ request.on(:response, &method(:finish_sentry_span).curry(3)[sentry_span, request])
18
+ end
19
+
20
+ def start_sentry_span
21
+ return unless ::Sentry.initialized? && (span = ::Sentry.get_current_scope.get_span)
22
+ return if span.sampled == false
23
+
24
+ span.start_child(op: "httpx.client", start_timestamp: ::Sentry.utc_now.to_f)
25
+ end
26
+
27
+ def set_sentry_trace_header(request, sentry_span)
28
+ return unless sentry_span
29
+
30
+ trace = ::Sentry.get_current_client.generate_sentry_trace(sentry_span)
31
+ request.headers[::Sentry::SENTRY_TRACE_HEADER_NAME] = trace if trace
32
+ end
33
+
34
+ def finish_sentry_span(span, request, response)
35
+ return unless ::Sentry.initialized?
36
+
37
+ record_sentry_breadcrumb(request, response)
38
+ record_sentry_span(request, response, span)
39
+ end
40
+
41
+ def record_sentry_breadcrumb(req, res)
42
+ return unless ::Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
43
+
44
+ request_info = extract_request_info(req)
45
+
46
+ data = if response.is_a?(HTTPX::ErrorResponse)
47
+ { error: res.message, **request_info }
48
+ else
49
+ { status: res.status, **request_info }
50
+ end
51
+
52
+ crumb = ::Sentry::Breadcrumb.new(
53
+ level: :info,
54
+ category: "httpx",
55
+ type: :info,
56
+ data: data
57
+ )
58
+ ::Sentry.add_breadcrumb(crumb)
59
+ end
60
+
61
+ def record_sentry_span(req, res, sentry_span)
62
+ return unless sentry_span
63
+
64
+ request_info = extract_request_info(req)
65
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
66
+ sentry_span.set_data(:status, res.status)
67
+ sentry_span.set_timestamp(::Sentry.utc_now.to_f)
68
+ end
69
+
70
+ def extract_request_info(req)
71
+ uri = req.uri
72
+
73
+ result = {
74
+ method: req.verb.to_s.upcase,
75
+ }
76
+
77
+ if ::Sentry.configuration.send_default_pii
78
+ uri += "?#{req.query}" unless req.query.empty?
79
+ result[:body] = req.body.to_s unless req.body.empty? || req.body.unbounded_body?
80
+ end
81
+
82
+ result[:url] = uri.to_s
83
+
84
+ result
85
+ end
86
+ end
87
+
88
+ module ConnectionMethods
89
+ def send(request)
90
+ Tracer.call(request)
91
+ super
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ Sentry.register_patch do
98
+ sentry_session = ::HTTPX.plugin(HTTPX::Plugins::Sentry)
99
+
100
+ HTTPX.send(:remove_const, :Session)
101
+ HTTPX.send(:const_set, :Session, sentry_session.class)
102
+ end
@@ -102,8 +102,8 @@ module HTTPX
102
102
  # origin came from an ORIGIN frame, we're going to verify the hostname with the
103
103
  # SSL certificate
104
104
  (@origins.size == 1 || @origin == uri.origin || (@io && @io.verify_hostname(uri.host)))
105
- ) || match_altsvcs?(uri)
106
- ) && @options == options
105
+ ) && @options == options
106
+ ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
107
107
  end
108
108
 
109
109
  def mergeable?(connection)
@@ -162,6 +162,14 @@ module HTTPX
162
162
  end
163
163
  end
164
164
 
165
+ def match_altsvc_options?(uri, options)
166
+ return @options == options unless @options.ssl[:hostname] == uri.host
167
+
168
+ dup_options = @options.merge(ssl: { hostname: nil })
169
+ dup_options.ssl.delete(:hostname)
170
+ dup_options == options
171
+ end
172
+
165
173
  def connecting?
166
174
  @state == :idle
167
175
  end
data/lib/httpx/io/ssl.rb CHANGED
@@ -4,8 +4,11 @@ require "openssl"
4
4
 
5
5
  module HTTPX
6
6
  TLSError = OpenSSL::SSL::SSLError
7
+ IPRegex = Regexp.union(Resolv::IPv4::Regex, Resolv::IPv6::Regex)
7
8
 
8
9
  class SSL < TCP
10
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
11
+
9
12
  TLS_OPTIONS = if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
10
13
  { alpn_protocols: %w[h2 http/1.1].freeze }.freeze
11
14
  else
@@ -19,6 +22,8 @@ module HTTPX
19
22
  @sni_hostname = ctx_options.delete(:hostname) || @hostname
20
23
  @ctx.set_params(ctx_options) unless ctx_options.empty?
21
24
  @state = :negotiated if @keep_open
25
+
26
+ @hostname_is_ip = IPRegex.match?(@sni_hostname)
22
27
  end
23
28
 
24
29
  def protocol
@@ -56,7 +61,7 @@ module HTTPX
56
61
 
57
62
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
58
63
  @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
59
- @io.hostname = @sni_hostname
64
+ @io.hostname = @sni_hostname unless @hostname_is_ip
60
65
  @io.sync_close = true
61
66
  end
62
67
  try_ssl_connect
@@ -66,7 +71,7 @@ module HTTPX
66
71
  # :nocov:
67
72
  def try_ssl_connect
68
73
  @io.connect_nonblock
69
- @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
74
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
70
75
  transition(:negotiated)
71
76
  @interests = :w
72
77
  rescue ::IO::WaitReadable
@@ -98,7 +103,7 @@ module HTTPX
98
103
  @interests = :w
99
104
  return
100
105
  end
101
- @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
106
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE && !@hostname_is_ip
102
107
  transition(:negotiated)
103
108
  @interests = :w
104
109
  end
data/lib/httpx/options.rb CHANGED
@@ -132,6 +132,7 @@ module HTTPX
132
132
  def freeze
133
133
  super
134
134
  @origin.freeze
135
+ @base_path.freeze
135
136
  @timeout.freeze
136
137
  @headers.freeze
137
138
  @addresses.freeze
@@ -141,6 +142,10 @@ module HTTPX
141
142
  URI(value)
142
143
  end
143
144
 
145
+ def option_base_path(value)
146
+ String(value)
147
+ end
148
+
144
149
  def option_headers(value)
145
150
  Headers.new(value)
146
151
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module HTTPX
6
+ module Plugins
7
+ module Authentication
8
+ class Basic
9
+ def initialize(user, password, **)
10
+ @user = user
11
+ @password = password
12
+ end
13
+
14
+ def can_authenticate?(authenticate)
15
+ authenticate && /Basic .*/.match?(authenticate)
16
+ end
17
+
18
+ def authenticate(*)
19
+ "Basic #{Base64.strict_encode64("#{@user}:#{@password}")}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require "securerandom"
5
+ require "digest"
6
+
7
+ module HTTPX
8
+ module Plugins
9
+ module Authentication
10
+ class Digest
11
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
12
+
13
+ def initialize(user, password, **)
14
+ @user = user
15
+ @password = password
16
+ @nonce = 0
17
+ end
18
+
19
+ def can_authenticate?(authenticate)
20
+ authenticate && /Digest .*/.match?(authenticate)
21
+ end
22
+
23
+ def authenticate(request, authenticate)
24
+ "Digest #{generate_header(request.verb.to_s.upcase, request.path, authenticate)}"
25
+ end
26
+
27
+ private
28
+
29
+ def generate_header(meth, uri, authenticate)
30
+ # discard first token, it's Digest
31
+ auth_info = authenticate[/^(\w+) (.*)/, 2]
32
+
33
+ params = Hash[auth_info.split(/ *, */)
34
+ .map { |val| val.split("=") }
35
+ .map { |k, v| [k, v.delete("\"")] }]
36
+ nonce = params["nonce"]
37
+ nc = next_nonce
38
+
39
+ # verify qop
40
+ qop = params["qop"]
41
+
42
+ if params["algorithm"] =~ /(.*?)(-sess)?$/
43
+ alg = Regexp.last_match(1)
44
+ algorithm = ::Digest.const_get(alg)
45
+ raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
46
+
47
+ sess = Regexp.last_match(2)
48
+ params.delete("algorithm")
49
+ else
50
+ algorithm = ::Digest::MD5
51
+ end
52
+
53
+ if qop || sess
54
+ cnonce = make_cnonce
55
+ nc = format("%<nonce>08x", nonce: nc)
56
+ end
57
+
58
+ a1 = if sess
59
+ [algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}"),
60
+ nonce,
61
+ cnonce].join ":"
62
+ else
63
+ "#{@user}:#{params["realm"]}:#{@password}"
64
+ end
65
+
66
+ ha1 = algorithm.hexdigest(a1)
67
+ ha2 = algorithm.hexdigest("#{meth}:#{uri}")
68
+ request_digest = [ha1, nonce]
69
+ request_digest.push(nc, cnonce, qop) if qop
70
+ request_digest << ha2
71
+ request_digest = request_digest.join(":")
72
+
73
+ header = [
74
+ %(username="#{@user}"),
75
+ %(nonce="#{nonce}"),
76
+ %(uri="#{uri}"),
77
+ %(response="#{algorithm.hexdigest(request_digest)}"),
78
+ ]
79
+ header << %(realm="#{params["realm"]}") if params.key?("realm")
80
+ header << %(algorithm=#{params["algorithm"]}") if params.key?("algorithm")
81
+ header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
82
+ header << %(cnonce="#{cnonce}") if cnonce
83
+ header << %(nc=#{nc})
84
+ header << %(qop=#{qop}) if qop
85
+ header.join ", "
86
+ end
87
+
88
+ def make_cnonce
89
+ ::Digest::MD5.hexdigest [
90
+ Time.now.to_i,
91
+ Process.pid,
92
+ SecureRandom.random_number(2**32),
93
+ ].join ":"
94
+ end
95
+
96
+ def next_nonce
97
+ @nonce += 1
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "ntlm"
5
+
6
+ module HTTPX
7
+ module Plugins
8
+ module Authentication
9
+ class Ntlm
10
+ using RegexpExtensions unless Regexp.method_defined?(:match?)
11
+
12
+ def initialize(user, password, domain: nil)
13
+ @user = user
14
+ @password = password
15
+ @domain = domain
16
+ end
17
+
18
+ def can_authenticate?(authenticate)
19
+ authenticate && /NTLM .*/.match?(authenticate)
20
+ end
21
+
22
+ def negotiate
23
+ "NTLM #{NTLM.negotiate(domain: @domain).to_base64}"
24
+ end
25
+
26
+ def authenticate(_req, www)
27
+ challenge = www[/NTLM (.*)/, 1]
28
+
29
+ challenge = Base64.decode64(challenge)
30
+ ntlm_challenge = NTLM.authenticate(challenge, @user, @domain, @password).to_base64
31
+
32
+ "NTLM #{ntlm_challenge}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+
5
+ module HTTPX
6
+ module Plugins
7
+ module Authentication
8
+ class Socks5
9
+ def initialize(user, password, **)
10
+ @user = user
11
+ @password = password
12
+ end
13
+
14
+ def can_authenticate?(*)
15
+ @user && @password
16
+ end
17
+
18
+ def authenticate(*)
19
+ [0x01, @user.bytesize, @user, @password.bytesize, @password].pack("CCA*CA*")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -7,10 +7,10 @@ module HTTPX
7
7
  #
8
8
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#basic-authentication
9
9
  #
10
- module BasicAuthentication
10
+ module BasicAuth
11
11
  class << self
12
12
  def load_dependencies(_klass)
13
- require "base64"
13
+ require_relative "authentication/basic"
14
14
  end
15
15
 
16
16
  def configure(klass)
@@ -19,12 +19,12 @@ module HTTPX
19
19
  end
20
20
 
21
21
  module InstanceMethods
22
- def basic_authentication(user, password)
23
- authentication("Basic #{Base64.strict_encode64("#{user}:#{password}")}")
22
+ def basic_auth(user, password)
23
+ authentication(Authentication::Basic.new(user, password).authenticate)
24
24
  end
25
- alias_method :basic_auth, :basic_authentication
25
+ alias_method :basic_authentication, :basic_auth
26
26
  end
27
27
  end
28
- register_plugin :basic_authentication, BasicAuthentication
28
+ register_plugin :basic_authentication, BasicAuth
29
29
  end
30
30
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
-
5
3
  module HTTPX
6
4
  module Plugins
7
5
  #
@@ -9,9 +7,7 @@ module HTTPX
9
7
  #
10
8
  # https://gitlab.com/honeyryderchuck/httpx/wikis/Authentication#authentication
11
9
  #
12
- module DigestAuthentication
13
- using RegexpExtensions unless Regexp.method_defined?(:match?)
14
-
10
+ module DigestAuth
15
11
  DigestError = Class.new(Error)
16
12
 
17
13
  class << self
@@ -20,14 +16,13 @@ module HTTPX
20
16
  end
21
17
 
22
18
  def load_dependencies(*)
23
- require "securerandom"
24
- require "digest"
19
+ require_relative "authentication/digest"
25
20
  end
26
21
  end
27
22
 
28
23
  module OptionsMethods
29
24
  def option_digest(value)
30
- raise TypeError, ":digest must be a Digest" unless value.is_a?(Digest)
25
+ raise TypeError, ":digest must be a Digest" unless value.is_a?(Authentication::Digest)
31
26
 
32
27
  value
33
28
  end
@@ -35,7 +30,7 @@ module HTTPX
35
30
 
36
31
  module InstanceMethods
37
32
  def digest_authentication(user, password)
38
- with(digest: Digest.new(user, password))
33
+ with(digest: Authentication::Digest.new(user, password))
39
34
  end
40
35
 
41
36
  alias_method :digest_auth, :digest_authentication
@@ -44,116 +39,25 @@ module HTTPX
44
39
  requests.flat_map do |request|
45
40
  digest = request.options.digest
46
41
 
47
- if digest
48
- probe_response = wrap { super(request).first }
49
-
50
- if digest && !probe_response.is_a?(ErrorResponse) &&
51
- probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
52
- /Digest .*/.match?(probe_response.headers["www-authenticate"])
53
-
54
- request.transition(:idle)
55
-
56
- token = digest.generate_header(request, probe_response)
57
- request.headers["authorization"] = "Digest #{token}"
58
-
59
- super(request)
60
- else
61
- probe_response
62
- end
63
- else
42
+ unless digest
64
43
  super(request)
44
+ next
65
45
  end
66
- end
67
- end
68
- end
69
-
70
- class Digest
71
- def initialize(user, password)
72
- @user = user
73
- @password = password
74
- @nonce = 0
75
- end
76
-
77
- def generate_header(request, response, _iis = false)
78
- meth = request.verb.to_s.upcase
79
- www = response.headers["www-authenticate"]
80
-
81
- # discard first token, it's Digest
82
- auth_info = www[/^(\w+) (.*)/, 2]
83
-
84
- uri = request.path
85
-
86
- params = Hash[auth_info.split(/ *, */)
87
- .map { |val| val.split("=") }
88
- .map { |k, v| [k, v.delete("\"")] }]
89
- nonce = params["nonce"]
90
- nc = next_nonce
91
46
 
92
- # verify qop
93
- qop = params["qop"]
47
+ probe_response = wrap { super(request).first }
94
48
 
95
- if params["algorithm"] =~ /(.*?)(-sess)?$/
96
- alg = Regexp.last_match(1)
97
- algorithm = ::Digest.const_get(alg)
98
- raise DigestError, "unknown algorithm \"#{alg}\"" unless algorithm
99
-
100
- sess = Regexp.last_match(2)
101
- params.delete("algorithm")
102
- else
103
- algorithm = ::Digest::MD5
104
- end
105
-
106
- if qop || sess
107
- cnonce = make_cnonce
108
- nc = format("%<nonce>08x", nonce: nc)
109
- end
110
-
111
- a1 = if sess
112
- [algorithm.hexdigest("#{@user}:#{params["realm"]}:#{@password}"),
113
- nonce,
114
- cnonce].join ":"
115
- else
116
- "#{@user}:#{params["realm"]}:#{@password}"
49
+ if probe_response.status == 401 && digest.can_authenticate?(probe_response.headers["www-authenticate"])
50
+ request.transition(:idle)
51
+ request.headers["authorization"] = digest.authenticate(request, probe_response.headers["www-authenticate"])
52
+ super(request)
53
+ else
54
+ probe_response
55
+ end
117
56
  end
118
-
119
- ha1 = algorithm.hexdigest(a1)
120
- ha2 = algorithm.hexdigest("#{meth}:#{uri}")
121
- request_digest = [ha1, nonce]
122
- request_digest.push(nc, cnonce, qop) if qop
123
- request_digest << ha2
124
- request_digest = request_digest.join(":")
125
-
126
- header = [
127
- %(username="#{@user}"),
128
- %(nonce="#{nonce}"),
129
- %(uri="#{uri}"),
130
- %(response="#{algorithm.hexdigest(request_digest)}"),
131
- ]
132
- header << %(realm="#{params["realm"]}") if params.key?("realm")
133
- header << %(algorithm=#{params["algorithm"]}") if params.key?("algorithm")
134
- header << %(opaque="#{params["opaque"]}") if params.key?("opaque")
135
- header << %(cnonce="#{cnonce}") if cnonce
136
- header << %(nc=#{nc})
137
- header << %(qop=#{qop}) if qop
138
- header.join ", "
139
- end
140
-
141
- private
142
-
143
- def make_cnonce
144
- ::Digest::MD5.hexdigest [
145
- Time.now.to_i,
146
- Process.pid,
147
- SecureRandom.random_number(2**32),
148
- ].join ":"
149
- end
150
-
151
- def next_nonce
152
- @nonce += 1
153
57
  end
154
58
  end
155
59
  end
156
60
 
157
- register_plugin :digest_authentication, DigestAuthentication
61
+ register_plugin :digest_authentication, DigestAuth
158
62
  end
159
63
  end
@@ -44,7 +44,7 @@ module HTTPX
44
44
 
45
45
  max_redirects = redirect_request.max_redirects
46
46
 
47
- return response unless REDIRECT_STATUS.include?(response.status)
47
+ return response unless REDIRECT_STATUS.include?(response.status) && response.headers.key?("location")
48
48
  return response unless max_redirects.positive?
49
49
 
50
50
  retry_request = build_redirect_request(redirect_request, response, options)
@@ -86,10 +86,22 @@ module HTTPX
86
86
  redirect_uri = __get_location_from_response(response)
87
87
  max_redirects = request.max_redirects
88
88
 
89
- # redirects are **ALWAYS** GET
90
- retry_options = options.merge(headers: request.headers,
91
- body: request.body,
92
- max_redirects: max_redirects - 1)
89
+ if response.status == 305 && options.respond_to?(:proxy)
90
+ # The requested resource MUST be accessed through the proxy given by
91
+ # the Location field. The Location field gives the URI of the proxy.
92
+ retry_options = options.merge(headers: request.headers,
93
+ proxy: { uri: redirect_uri },
94
+ body: request.body,
95
+ max_redirects: max_redirects - 1)
96
+ redirect_uri = request.url
97
+ else
98
+
99
+ # redirects are **ALWAYS** GET
100
+ retry_options = options.merge(headers: request.headers,
101
+ body: request.body,
102
+ max_redirects: max_redirects - 1)
103
+ end
104
+
93
105
  build_request(:get, redirect_uri, retry_options)
94
106
  end
95
107