httpx 1.7.2 → 1.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/doc/release_notes/1_7_3.md +29 -0
  4. data/doc/release_notes/1_7_4.md +42 -0
  5. data/lib/httpx/adapters/datadog.rb +24 -60
  6. data/lib/httpx/adapters/webmock.rb +3 -4
  7. data/lib/httpx/connection/http1.rb +6 -1
  8. data/lib/httpx/connection/http2.rb +43 -30
  9. data/lib/httpx/connection.rb +74 -22
  10. data/lib/httpx/plugins/auth/digest.rb +2 -1
  11. data/lib/httpx/plugins/brotli.rb +33 -5
  12. data/lib/httpx/plugins/cookies/cookie.rb +34 -11
  13. data/lib/httpx/plugins/cookies/jar.rb +93 -18
  14. data/lib/httpx/plugins/cookies.rb +7 -3
  15. data/lib/httpx/plugins/expect.rb +30 -3
  16. data/lib/httpx/plugins/fiber_concurrency.rb +2 -4
  17. data/lib/httpx/plugins/follow_redirects.rb +7 -1
  18. data/lib/httpx/plugins/h2c.rb +1 -1
  19. data/lib/httpx/plugins/proxy/http.rb +15 -8
  20. data/lib/httpx/plugins/proxy.rb +10 -2
  21. data/lib/httpx/plugins/rate_limiter.rb +19 -19
  22. data/lib/httpx/plugins/retries.rb +17 -9
  23. data/lib/httpx/plugins/ssrf_filter.rb +1 -0
  24. data/lib/httpx/plugins/stream_bidi.rb +6 -0
  25. data/lib/httpx/plugins/tracing.rb +137 -0
  26. data/lib/httpx/request.rb +1 -1
  27. data/lib/httpx/resolver/multi.rb +1 -8
  28. data/lib/httpx/resolver/native.rb +1 -1
  29. data/lib/httpx/resolver/resolver.rb +21 -2
  30. data/lib/httpx/resolver/system.rb +3 -1
  31. data/lib/httpx/selector.rb +4 -4
  32. data/lib/httpx/session.rb +11 -6
  33. data/lib/httpx/version.rb +1 -1
  34. data/sig/chainable.rbs +2 -1
  35. data/sig/connection/http1.rbs +2 -0
  36. data/sig/connection/http2.rbs +11 -4
  37. data/sig/connection.rbs +7 -0
  38. data/sig/plugins/brotli.rbs +11 -6
  39. data/sig/plugins/cookies/cookie.rbs +3 -2
  40. data/sig/plugins/cookies/jar.rbs +11 -0
  41. data/sig/plugins/cookies.rbs +2 -0
  42. data/sig/plugins/expect.rbs +17 -2
  43. data/sig/plugins/proxy/socks4.rbs +4 -0
  44. data/sig/plugins/rate_limiter.rbs +2 -2
  45. data/sig/plugins/response_cache.rbs +3 -3
  46. data/sig/plugins/retries.rbs +17 -13
  47. data/sig/plugins/tracing.rbs +41 -0
  48. data/sig/request.rbs +1 -0
  49. data/sig/resolver/native.rbs +2 -0
  50. data/sig/resolver/resolver.rbs +4 -2
  51. data/sig/resolver/system.rbs +0 -2
  52. data/sig/response/body.rbs +1 -1
  53. data/sig/selector.rbs +4 -0
  54. data/sig/session.rbs +2 -0
  55. data/sig/transcoder/gzip.rbs +1 -1
  56. data/sig/transcoder.rbs +0 -2
  57. metadata +9 -3
@@ -14,12 +14,14 @@ module HTTPX
14
14
 
15
15
  attr_reader :domain, :path, :name, :value, :created_at
16
16
 
17
+ # assigns a new +path+ to this cookie.
17
18
  def path=(path)
18
19
  path = String(path)
20
+ @for_domain = false
19
21
  @path = path.start_with?("/") ? path : "/"
20
22
  end
21
23
 
22
- # See #domain.
24
+ # assigns a new +domain+ to this cookie.
23
25
  def domain=(domain)
24
26
  domain = String(domain)
25
27
 
@@ -37,6 +39,13 @@ module HTTPX
37
39
  @domain = @domain_name.hostname
38
40
  end
39
41
 
42
+ # checks whether +other+ is the same cookie, i.e. name, value, domain and path are
43
+ # the same.
44
+ def ==(other)
45
+ @name == other.name && @value == other.value &&
46
+ @path == other.path && @domain == other.domain
47
+ end
48
+
40
49
  # Compares the cookie with another. When there are many cookies with
41
50
  # the same name for a URL, the value of the smallest must be used.
42
51
  def <=>(other)
@@ -47,11 +56,29 @@ module HTTPX
47
56
  (@created_at <=> other.created_at).nonzero? || 0
48
57
  end
49
58
 
59
+ def match?(name_or_options)
60
+ case name_or_options
61
+ when String
62
+ @name == name_or_options
63
+ when Hash, Array
64
+ name_or_options.all? { |k, v| respond_to?(k) && send(k) == v }
65
+ else
66
+ false
67
+ end
68
+ end
69
+
50
70
  class << self
51
71
  def new(cookie, *args)
52
- return cookie if cookie.is_a?(self)
72
+ case cookie
73
+ when self
74
+ cookie
75
+ when Array, Hash
76
+ options = Hash[cookie] #: cookie_attributes
77
+ super(options[:name], options[:value], options)
78
+ else
53
79
 
54
- super
80
+ super
81
+ end
55
82
  end
56
83
 
57
84
  # Tests if +target_path+ is under +base_path+ as described in RFC
@@ -84,16 +111,12 @@ module HTTPX
84
111
  end
85
112
  end
86
113
 
87
- def initialize(arg, *attrs)
114
+ def initialize(arg, value, attrs = nil)
88
115
  @created_at = Time.now
89
116
 
90
- if attrs.empty?
91
- attr_hash = Hash.try_convert(arg)
92
- else
93
- @name = arg
94
- @value, attr_hash = attrs
95
- attr_hash = Hash.try_convert(attr_hash)
96
- end
117
+ @name = arg
118
+ @value = value
119
+ attr_hash = Hash.try_convert(attrs)
97
120
 
98
121
  attr_hash.each do |key, val|
99
122
  key = key.downcase.tr("-", "_").to_sym unless key.is_a?(Symbol)
@@ -4,7 +4,12 @@ module HTTPX
4
4
  module Plugins::Cookies
5
5
  # The Cookie Jar
6
6
  #
7
- # It holds a bunch of cookies.
7
+ # It stores and manages cookies for a session, such as i.e. evicting when expired, access methods, or
8
+ # initialization from parsing `Set-Cookie` HTTP header values.
9
+ #
10
+ # It closely follows the [CookieStore API](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore),
11
+ # by implementing the same methods, with a few specific conveniences for this non-browser manipulation use-case.
12
+ #
8
13
  class Jar
9
14
  using URIExtensions
10
15
 
@@ -12,10 +17,14 @@ module HTTPX
12
17
 
13
18
  def initialize_dup(orig)
14
19
  super
20
+ @mtx = orig.instance_variable_get(:@mtx).dup
15
21
  @cookies = orig.instance_variable_get(:@cookies).dup
16
22
  end
17
23
 
24
+ # initializes the cookie store, either empty, or with whatever is passed as +cookies+, which
25
+ # can be an array of HTTPX::Plugins::Cookies::Cookie objects or hashes-or-tuples of cookie attributes.
18
26
  def initialize(cookies = nil)
27
+ @mtx = Thread::Mutex.new
19
28
  @cookies = []
20
29
 
21
30
  cookies.each do |elem|
@@ -32,48 +41,106 @@ module HTTPX
32
41
  end if cookies
33
42
  end
34
43
 
44
+ # parses the `Set-Cookie` header value as +set_cookie+ and does the corresponding updates.
35
45
  def parse(set_cookie)
36
46
  SetCookieParser.call(set_cookie) do |name, value, attrs|
37
- add(Cookie.new(name, value, attrs))
47
+ set(Cookie.new(name, value, attrs))
48
+ end
49
+ end
50
+
51
+ # returns the first HTTPX::Plugins::Cookie::Cookie instance in the store which matches either the name
52
+ # (when String) or all attributes (when a Hash or array of tuples) passed to +name_or_options+
53
+ def get(name_or_options)
54
+ each.find { |ck| ck.match?(name_or_options) }
55
+ end
56
+
57
+ # returns all HTTPX::Plugins::Cookie::Cookie instances in the store which match either the name
58
+ # (when String) or all attributes (when a Hash or array of tuples) passed to +name_or_options+
59
+ def get_all(name_or_options)
60
+ each.select { |ck| ck.match?(name_or_options) } # rubocop:disable Style/SelectByRegexp
61
+ end
62
+
63
+ # when +name+ is a HTTPX::Plugins::Cookie::Cookie, it stores it internally; when +name+ is a String,
64
+ # it creates a cookie with it and the value-or-attributes passed to +value_or_options+.
65
+
66
+ # optionally, +name+ can also be the attributes hash-or-array as long it contains a <tt>:name</tt> field).
67
+ def set(name, value_or_options = nil)
68
+ cookie = case name
69
+ when Cookie
70
+ raise ArgumentError, "there should not be a second argument" if value_or_options
71
+
72
+ name
73
+ when Array, Hash
74
+ raise ArgumentError, "there should not be a second argument" if value_or_options
75
+
76
+ Cookie.new(name)
77
+ else
78
+ raise ArgumentError, "the second argument is required" unless value_or_options
79
+
80
+ Cookie.new(name, value_or_options)
81
+ end
82
+
83
+ synchronize do
84
+ # If the user agent receives a new cookie with the same cookie-name, domain-value, and path-value
85
+ # as a cookie that it has already stored, the existing cookie is evicted and replaced with the new cookie.
86
+ @cookies.delete_if { |ck| ck.name == cookie.name && ck.domain == cookie.domain && ck.path == cookie.path }
87
+
88
+ @cookies << cookie
38
89
  end
39
90
  end
40
91
 
92
+ # @deprecated
41
93
  def add(cookie, path = nil)
94
+ warn "DEPRECATION WARNING: calling `##{__method__}` is deprecated. Use `#set` instead."
42
95
  c = cookie.dup
43
-
44
96
  c.path = path if path && c.path == "/"
97
+ set(c)
98
+ end
45
99
 
46
- # If the user agent receives a new cookie with the same cookie-name, domain-value, and path-value
47
- # as a cookie that it has already stored, the existing cookie is evicted and replaced with the new cookie.
48
- @cookies.delete_if { |ck| ck.name == c.name && ck.domain == c.domain && ck.path == c.path }
49
-
50
- @cookies << c
100
+ # deletes all cookies in the store which match either the name (when String) or all attributes (when a Hash
101
+ # or array of tuples) passed to +name_or_options+.
102
+ #
103
+ # alternatively, of +name_or_options+ is an instance of HTTPX::Plugins::Cookies::Cookiem, it deletes it from the store.
104
+ def delete(name_or_options)
105
+ synchronize do
106
+ case name_or_options
107
+ when Cookie
108
+ @cookies.delete(name_or_options)
109
+ else
110
+ @cookies.delete_if { |ck| ck.match?(name_or_options) }
111
+ end
112
+ end
51
113
  end
52
114
 
115
+ # returns the list of valid cookies which matdh the domain and path from the URI object passed to +uri+.
53
116
  def [](uri)
54
117
  each(uri).sort
55
118
  end
56
119
 
120
+ # enumerates over all stored cookies. if +uri+ is passed, it'll filter out expired cookies and
121
+ # only yield cookies which match its domain and path.
57
122
  def each(uri = nil, &blk)
58
123
  return enum_for(__method__, uri) unless blk
59
124
 
60
- return @cookies.each(&blk) unless uri
125
+ return synchronize { @cookies.each(&blk) } unless uri
61
126
 
62
127
  now = Time.now
63
128
  tpath = uri.path
64
129
 
65
- @cookies.delete_if do |cookie|
66
- if cookie.expired?(now)
67
- true
68
- else
69
- yield cookie if cookie.valid_for_uri?(uri) && Cookie.path_match?(cookie.path, tpath)
70
- false
130
+ synchronize do
131
+ @cookies.delete_if do |cookie|
132
+ if cookie.expired?(now)
133
+ true
134
+ else
135
+ yield cookie if cookie.valid_for_uri?(uri) && Cookie.path_match?(cookie.path, tpath)
136
+ false
137
+ end
71
138
  end
72
139
  end
73
140
  end
74
141
 
75
142
  def merge(other)
76
- cookies_dup = dup
143
+ jar_dup = dup
77
144
 
78
145
  other.each do |elem|
79
146
  cookie = case elem
@@ -85,10 +152,18 @@ module HTTPX
85
152
  Cookie.new(elem)
86
153
  end
87
154
 
88
- cookies_dup.add(cookie)
155
+ jar_dup.set(cookie)
89
156
  end
90
157
 
91
- cookies_dup
158
+ jar_dup
159
+ end
160
+
161
+ private
162
+
163
+ def synchronize(&block)
164
+ return yield if @mtx.owned?
165
+
166
+ @mtx.synchronize(&block)
92
167
  end
93
168
  end
94
169
  end
@@ -7,8 +7,6 @@ module HTTPX
7
7
  #
8
8
  # This plugin implements a persistent cookie jar for the duration of a session.
9
9
  #
10
- # It also adds a *#cookies* helper, so that you can pre-fill the cookies of a session.
11
- #
12
10
  # https://gitlab.com/os85/httpx/wikis/Cookies
13
11
  #
14
12
  module Cookies
@@ -46,6 +44,12 @@ module HTTPX
46
44
  request
47
45
  end
48
46
 
47
+ # factory method to return a Jar to the user, which can then manipulate
48
+ # externally to the session.
49
+ def make_jar(*args)
50
+ Jar.new(*args)
51
+ end
52
+
49
53
  private
50
54
 
51
55
  def set_request_callbacks(request)
@@ -96,7 +100,7 @@ module HTTPX
96
100
  cookies.each do |ck|
97
101
  ck.split(/ *; */).each do |cookie|
98
102
  name, value = cookie.split("=", 2)
99
- jar.add(Cookie.new(name, value))
103
+ jar.set(name, value)
100
104
  end
101
105
  end
102
106
  end
@@ -9,10 +9,34 @@ module HTTPX
9
9
  #
10
10
  module Expect
11
11
  EXPECT_TIMEOUT = 2
12
+ NOEXPECT_STORE_MUTEX = Thread::Mutex.new
13
+
14
+ class Store
15
+ def initialize
16
+ @store = []
17
+ @mutex = Thread::Mutex.new
18
+ end
19
+
20
+ def include?(host)
21
+ @mutex.synchronize { @store.include?(host) }
22
+ end
23
+
24
+ def add(host)
25
+ @mutex.synchronize { @store << host }
26
+ end
27
+
28
+ def delete(host)
29
+ @mutex.synchronize { @store.delete(host) }
30
+ end
31
+ end
12
32
 
13
33
  class << self
14
34
  def no_expect_store
15
- @no_expect_store ||= []
35
+ return Ractor.store_if_absent(:httpx_no_expect_store) { Store.new } if Utils.in_ractor?
36
+
37
+ @no_expect_store ||= NOEXPECT_STORE_MUTEX.synchronize do
38
+ @no_expect_store || Store.new
39
+ end
16
40
  end
17
41
 
18
42
  def extra_options(options)
@@ -89,7 +113,7 @@ module HTTPX
89
113
  set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
90
114
  # expect timeout expired
91
115
  if request.state == :expect && !request.expects?
92
- Expect.no_expect_store << request.origin
116
+ Expect.no_expect_store.add(request.origin)
93
117
  request.headers.delete("expect")
94
118
  consume
95
119
  end
@@ -108,7 +132,10 @@ module HTTPX
108
132
  request.headers.delete("expect")
109
133
  request.transition(:idle)
110
134
  send_request(request, selector, options)
111
- return
135
+
136
+ # recalling itself, in case an error was triggered by the above, and we can
137
+ # verify retriability again.
138
+ return fetch_response(request, selector, options)
112
139
  end
113
140
 
114
141
  response
@@ -160,9 +160,7 @@ module HTTPX
160
160
  end
161
161
  end
162
162
 
163
- module NativeResolverMethods
164
- private
165
-
163
+ module ResolverNativeMethods
166
164
  def calculate_interests
167
165
  return if @queries.empty?
168
166
 
@@ -172,7 +170,7 @@ module HTTPX
172
170
  end
173
171
  end
174
172
 
175
- module SystemResolverMethods
173
+ module ResolverSystemMethods
176
174
  def interests
177
175
  return unless @queries.any? { |_, conn| conn.current_context? }
178
176
 
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- InsecureRedirectError = Class.new(Error)
4
+ class InsecureRedirectError < Error
5
+ end
6
+
5
7
  module Plugins
6
8
  #
7
9
  # This plugin adds support for automatically following redirect (status 30X) responses.
@@ -163,6 +165,10 @@ module HTTPX
163
165
  end
164
166
  else
165
167
  send_request(retry_request, selector, options)
168
+
169
+ # recalling itself, in case an error was triggered by the above, and we can
170
+ # verify retriability again.
171
+ return fetch_response(request, selector, options)
166
172
  end
167
173
  nil
168
174
  end
@@ -81,7 +81,7 @@ module HTTPX
81
81
  @parser.upgrade(request, response)
82
82
  @upgrade_protocol = "h2c"
83
83
 
84
- prev_parser.requests.each do |req|
84
+ prev_parser.pending.each do |req|
85
85
  req.transition(:idle)
86
86
  send(req)
87
87
  end
@@ -35,7 +35,10 @@ module HTTPX
35
35
  request.headers["proxy-authorization"] =
36
36
  options.proxy.authenticate(request, response.headers["proxy-authenticate"])
37
37
  send_request(request, selector, options)
38
- return
38
+
39
+ # recalling itself, in case an error was triggered by the above, and we can
40
+ # verify retriability again.
41
+ return fetch_response(request, selector, options)
39
42
  end
40
43
 
41
44
  response
@@ -46,7 +49,7 @@ module HTTPX
46
49
  def force_close(*)
47
50
  if @state == :connecting
48
51
  # proxy connect related requests should not be reenqueed
49
- @parser.reset!
52
+ @parser.reset
50
53
  @inflight -= @parser.pending.size
51
54
  @parser.pending.clear
52
55
  end
@@ -67,18 +70,16 @@ module HTTPX
67
70
  return unless @io.connected?
68
71
 
69
72
  @parser || begin
70
- @parser = parser_type(@io.protocol).new(@write_buffer, @options.merge(max_concurrent_requests: 1))
71
- parser = @parser
73
+ @parser = parser = parser_type(@io.protocol).new(@write_buffer, @options.merge(max_concurrent_requests: 1))
72
74
  parser.extend(ProxyParser)
73
75
  parser.on(:response, &method(:__http_on_connect))
74
76
  parser.on(:close) do
75
77
  next unless @parser
76
78
 
77
79
  reset
78
- disconnect
79
80
  end
80
81
  parser.on(:reset) do
81
- if parser.empty?
82
+ if parser.pending.empty? && parser.empty?
82
83
  reset
83
84
  else
84
85
  enqueue_pending_requests_from_parser(parser)
@@ -94,17 +95,23 @@ module HTTPX
94
95
  # keep parser state around due to proxy auth protocol;
95
96
  # intermediate authenticated request is already inside
96
97
  # the parser
97
- parser = nil
98
+ connect_request = parser = nil
98
99
 
99
100
  if initial_state == :connecting
100
101
  parser = @parser
101
102
  @parser.reset
103
+ if @pending.first.is_a?(ConnectRequest)
104
+ connect_request = @pending.shift # this happened when reenqueing
105
+ end
102
106
  end
103
107
 
104
108
  idling
105
109
 
106
110
  @parser = parser
107
-
111
+ if connect_request
112
+ @inflight += 1
113
+ parser.send(connect_request)
114
+ end
108
115
  transition(:connecting)
109
116
  end
110
117
  end
@@ -202,7 +202,10 @@ module HTTPX
202
202
  log { "failed connecting to proxy, trying next..." }
203
203
  request.transition(:idle)
204
204
  send_request(request, selector, options)
205
- return
205
+
206
+ # recalling itself, in case an error was triggered by the above, and we can
207
+ # verify retriability again.
208
+ return fetch_response(request, selector, options)
206
209
  end
207
210
  response
208
211
  rescue ProxyError
@@ -320,7 +323,12 @@ module HTTPX
320
323
 
321
324
  def purge_after_closed
322
325
  super
323
- @io = @io.proxy_io if @io.respond_to?(:proxy_io)
326
+
327
+ while @io.respond_to?(:proxy_io)
328
+ @io = @io.proxy_io
329
+
330
+ super
331
+ end
324
332
  end
325
333
  end
326
334
 
@@ -16,25 +16,7 @@ module HTTPX
16
16
 
17
17
  class << self
18
18
  def load_dependencies(klass)
19
- klass.plugin(:retries, retry_after: method(:retry_after_rate_limit))
20
- end
21
-
22
- # Servers send the "Retry-After" header field to indicate how long the
23
- # user agent ought to wait before making a follow-up request. When
24
- # sent with a 503 (Service Unavailable) response, Retry-After indicates
25
- # how long the service is expected to be unavailable to the client.
26
- # When sent with any 3xx (Redirection) response, Retry-After indicates
27
- # the minimum time that the user agent is asked to wait before issuing
28
- # the redirected request.
29
- #
30
- def retry_after_rate_limit(_, response)
31
- return unless response.is_a?(Response)
32
-
33
- retry_after = response.headers["retry-after"]
34
-
35
- return unless retry_after
36
-
37
- Utils.parse_retry_after(retry_after)
19
+ klass.plugin(:retries)
38
20
  end
39
21
  end
40
22
 
@@ -52,6 +34,24 @@ module HTTPX
52
34
  def rate_limit_error?(response)
53
35
  response.is_a?(Response) && RATE_LIMIT_CODES.include?(response.status)
54
36
  end
37
+
38
+ # Servers send the "Retry-After" header field to indicate how long the
39
+ # user agent ought to wait before making a follow-up request. When
40
+ # sent with a 503 (Service Unavailable) response, Retry-After indicates
41
+ # how long the service is expected to be unavailable to the client.
42
+ # When sent with any 3xx (Redirection) response, Retry-After indicates
43
+ # the minimum time that the user agent is asked to wait before issuing
44
+ # the redirected request.
45
+ #
46
+ def when_to_retry(_, response, options)
47
+ return super unless response.is_a?(Response)
48
+
49
+ retry_after = response.headers["retry-after"]
50
+
51
+ return super unless retry_after
52
+
53
+ Utils.parse_retry_after(retry_after)
54
+ end
55
55
  end
56
56
  end
57
57
 
@@ -148,10 +148,7 @@ module HTTPX
148
148
  log { "failed to get response, #{request.retries} tries to go..." }
149
149
  prepare_to_retry(request, response)
150
150
 
151
- retry_after = options.retry_after
152
- retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
153
-
154
- if retry_after
151
+ if (retry_after = when_to_retry(request, response, options))
155
152
  # apply jitter
156
153
  if (jitter = request.options.retry_jitter)
157
154
  retry_after = jitter.call(retry_after)
@@ -169,11 +166,15 @@ module HTTPX
169
166
  send_request(request, selector, options)
170
167
  end
171
168
  end
169
+
170
+ return
172
171
  else
173
172
  send_request(request, selector, options)
174
- end
175
173
 
176
- return
174
+ # recalling itself, in case an error was triggered by the above, and we can
175
+ # verify retriability again.
176
+ return fetch_response(request, selector, options)
177
+ end
177
178
  end
178
179
  response
179
180
  end
@@ -201,6 +202,12 @@ module HTTPX
201
202
  request.transition(:idle)
202
203
  end
203
204
 
205
+ def when_to_retry(request, response, options)
206
+ retry_after = options.retry_after
207
+ retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
208
+ retry_after
209
+ end
210
+
204
211
  #
205
212
  # Attempt to set the request to perform a partial range request.
206
213
  # This happens if the peer server accepts byte-range requests, and
@@ -237,14 +244,15 @@ module HTTPX
237
244
  def initialize(*args)
238
245
  super
239
246
  @retries = @options.max_retries
247
+ @partial_response = nil
240
248
  end
241
249
 
242
250
  def response=(response)
243
- if @partial_response
251
+ if (partial_response = @partial_response)
244
252
  if response.is_a?(Response) && response.status == 206
245
- response.from_partial_response(@partial_response)
253
+ response.from_partial_response(partial_response)
246
254
  else
247
- @partial_response.close
255
+ partial_response.close
248
256
  end
249
257
  @partial_response = nil
250
258
  end
@@ -106,6 +106,7 @@ module HTTPX
106
106
  error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
107
107
  error.set_backtrace(caller)
108
108
  response = ErrorResponse.new(request, error)
109
+ request.response = response
109
110
  request.emit(:response, response)
110
111
  response
111
112
  end
@@ -189,6 +189,10 @@ module HTTPX
189
189
  !@closed
190
190
  end
191
191
 
192
+ def force_close(*)
193
+ terminate
194
+ end
195
+
192
196
  def terminate
193
197
  return if @closed
194
198
 
@@ -202,6 +206,8 @@ module HTTPX
202
206
  terminate
203
207
  end
204
208
 
209
+ alias_method :on_io_error, :on_error
210
+
205
211
  # noop (the owner connection will take of it)
206
212
  def handle_socket_timeout(interval); end
207
213
  end