httpx 1.1.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/doc/release_notes/1_1_4.md +1 -1
  4. data/doc/release_notes/1_1_5.md +12 -0
  5. data/doc/release_notes/1_2_0.md +49 -0
  6. data/lib/httpx/adapters/webmock.rb +29 -8
  7. data/lib/httpx/altsvc.rb +57 -2
  8. data/lib/httpx/chainable.rb +40 -29
  9. data/lib/httpx/connection/http1.rb +27 -22
  10. data/lib/httpx/connection/http2.rb +7 -3
  11. data/lib/httpx/connection.rb +45 -60
  12. data/lib/httpx/extensions.rb +0 -15
  13. data/lib/httpx/options.rb +84 -27
  14. data/lib/httpx/plugins/aws_sigv4.rb +2 -2
  15. data/lib/httpx/plugins/basic_auth.rb +1 -1
  16. data/lib/httpx/plugins/callbacks.rb +91 -0
  17. data/lib/httpx/plugins/circuit_breaker.rb +2 -0
  18. data/lib/httpx/plugins/cookies.rb +19 -9
  19. data/lib/httpx/plugins/digest_auth.rb +1 -1
  20. data/lib/httpx/plugins/follow_redirects.rb +11 -0
  21. data/lib/httpx/plugins/grpc/call.rb +2 -3
  22. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -0
  23. data/lib/httpx/plugins/grpc.rb +2 -2
  24. data/lib/httpx/plugins/h2c.rb +20 -8
  25. data/lib/httpx/plugins/proxy/socks4.rb +2 -2
  26. data/lib/httpx/plugins/proxy/socks5.rb +2 -2
  27. data/lib/httpx/plugins/proxy.rb +14 -32
  28. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  29. data/lib/httpx/plugins/retries.rb +4 -0
  30. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  31. data/lib/httpx/plugins/stream.rb +9 -4
  32. data/lib/httpx/plugins/upgrade/h2.rb +1 -1
  33. data/lib/httpx/plugins/upgrade.rb +1 -1
  34. data/lib/httpx/plugins/webdav.rb +1 -1
  35. data/lib/httpx/pool.rb +32 -28
  36. data/lib/httpx/request/body.rb +3 -3
  37. data/lib/httpx/request.rb +12 -3
  38. data/lib/httpx/resolver/https.rb +3 -2
  39. data/lib/httpx/resolver/native.rb +1 -0
  40. data/lib/httpx/resolver/resolver.rb +17 -6
  41. data/lib/httpx/response.rb +1 -1
  42. data/lib/httpx/session.rb +13 -82
  43. data/lib/httpx/timers.rb +3 -10
  44. data/lib/httpx/transcoder.rb +1 -1
  45. data/lib/httpx/version.rb +1 -1
  46. data/sig/altsvc.rbs +33 -0
  47. data/sig/chainable.rbs +1 -0
  48. data/sig/connection/http1.rbs +2 -1
  49. data/sig/connection.rbs +16 -16
  50. data/sig/options.rbs +10 -2
  51. data/sig/plugins/callbacks.rbs +38 -0
  52. data/sig/plugins/cookies.rbs +2 -0
  53. data/sig/plugins/follow_redirects.rbs +2 -0
  54. data/sig/plugins/proxy/socks4.rbs +2 -1
  55. data/sig/plugins/proxy/socks5.rbs +2 -1
  56. data/sig/plugins/proxy.rbs +11 -1
  57. data/sig/plugins/stream.rbs +24 -22
  58. data/sig/pool.rbs +1 -3
  59. data/sig/resolver/resolver.rbs +3 -1
  60. data/sig/session.rbs +4 -4
  61. metadata +12 -4
@@ -4,9 +4,9 @@ module HTTPX
4
4
  module Plugins
5
5
  #
6
6
  # This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2
7
- # (https://tools.ietf.org/html/rfc7540#section-3.2)
7
+ # (https://datatracker.ietf.org/doc/html/rfc7540#section-3.2)
8
8
  #
9
- # https://gitlab.com/os85/httpx/wikis/Upgrade#h2c
9
+ # https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2c
10
10
  #
11
11
  module H2C
12
12
  VALID_H2C_VERBS = %w[GET OPTIONS HEAD].freeze
@@ -73,22 +73,34 @@ module HTTPX
73
73
  @inflight -= prev_parser.requests.size
74
74
  end
75
75
 
76
- parser_options = @options.merge(max_concurrent_requests: request.options.max_concurrent_requests)
77
- @parser = H2CParser.new(@write_buffer, parser_options)
76
+ @parser = H2CParser.new(@write_buffer, @options)
78
77
  set_parser_callbacks(@parser)
79
78
  @inflight += 1
80
79
  @parser.upgrade(request, response)
81
80
  @upgrade_protocol = "h2c"
82
81
 
83
- if request.options.max_concurrent_requests != @options.max_concurrent_requests
84
- @options = @options.merge(max_concurrent_requests: nil)
85
- end
86
-
87
82
  prev_parser.requests.each do |req|
88
83
  req.transition(:idle)
89
84
  send(req)
90
85
  end
91
86
  end
87
+
88
+ private
89
+
90
+ def send_request_to_parser(request)
91
+ super
92
+
93
+ return unless request.headers["upgrade"] == "h2c" && parser.is_a?(Connection::HTTP1)
94
+
95
+ max_concurrent_requests = parser.max_concurrent_requests
96
+
97
+ return if max_concurrent_requests == 1
98
+
99
+ parser.max_concurrent_requests = 1
100
+ request.once(:response) do
101
+ parser.max_concurrent_requests = max_concurrent_requests
102
+ end
103
+ end
92
104
  end
93
105
  end
94
106
  register_plugin(:h2c, H2C)
@@ -4,7 +4,7 @@ require "resolv"
4
4
  require "ipaddr"
5
5
 
6
6
  module HTTPX
7
- class Socks4Error < Error; end
7
+ class Socks4Error < HTTPProxyError; end
8
8
 
9
9
  module Plugins
10
10
  module Proxy
@@ -85,7 +85,7 @@ module HTTPX
85
85
  end
86
86
 
87
87
  class SocksParser
88
- include Callbacks
88
+ include HTTPX::Callbacks
89
89
 
90
90
  def initialize(buffer, options)
91
91
  @buffer = buffer
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- class Socks5Error < Error; end
4
+ class Socks5Error < HTTPProxyError; end
5
5
 
6
6
  module Plugins
7
7
  module Proxy
@@ -137,7 +137,7 @@ module HTTPX
137
137
  end
138
138
 
139
139
  class SocksParser
140
- include Callbacks
140
+ include HTTPX::Callbacks
141
141
 
142
142
  def initialize(buffer, options)
143
143
  @buffer = buffer
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- class HTTPProxyError < Error; end
4
+ class HTTPProxyError < ConnectionError; end
5
5
 
6
6
  module Plugins
7
7
  #
@@ -143,7 +143,7 @@ module HTTPX
143
143
  proxy = Parameters.new(**proxy_opts)
144
144
 
145
145
  proxy_options = options.merge(proxy: proxy)
146
- connection = pool.find_connection(uri, proxy_options) || build_connection(uri, proxy_options)
146
+ connection = pool.find_connection(uri, proxy_options) || init_connection(uri, proxy_options)
147
147
  unless connections.nil? || connections.include?(connection)
148
148
  connections << connection
149
149
  set_connection_callbacks(connection, connections, options)
@@ -151,19 +151,15 @@ module HTTPX
151
151
  connection
152
152
  end
153
153
 
154
- def build_connection(uri, options)
155
- proxy = options.proxy
156
- return super unless proxy
157
-
158
- init_connection("tcp", uri, options)
159
- end
160
-
161
154
  def fetch_response(request, connections, options)
162
155
  response = super
163
156
 
164
- if response.is_a?(ErrorResponse) &&
165
- __proxy_error?(response) && !@_proxy_uris.empty?
157
+ if response.is_a?(ErrorResponse) && proxy_error?(request, response)
166
158
  @_proxy_uris.shift
159
+
160
+ # return last error response if no more proxies to try
161
+ return response if @_proxy_uris.empty?
162
+
167
163
  log { "failed connecting to proxy, trying next..." }
168
164
  request.transition(:idle)
169
165
  send_request(request, connections, options)
@@ -172,13 +168,7 @@ module HTTPX
172
168
  response
173
169
  end
174
170
 
175
- def build_altsvc_connection(_, _, _, _, _, options)
176
- return if options.proxy
177
-
178
- super
179
- end
180
-
181
- def __proxy_error?(response)
171
+ def proxy_error?(_request, response)
182
172
  error = response.error
183
173
  case error
184
174
  when NativeResolveError
@@ -235,14 +225,6 @@ module HTTPX
235
225
  end
236
226
  end
237
227
 
238
- def send(request)
239
- return super unless (
240
- @options.proxy && @state != :idle && connecting?
241
- )
242
-
243
- (@proxy_pending ||= []) << request
244
- end
245
-
246
228
  def connecting?
247
229
  return super unless @options.proxy
248
230
 
@@ -271,6 +253,12 @@ module HTTPX
271
253
 
272
254
  private
273
255
 
256
+ def initialize_type(uri, options)
257
+ return super unless options.proxy
258
+
259
+ "tcp"
260
+ end
261
+
274
262
  def connect
275
263
  return super unless @options.proxy
276
264
 
@@ -278,12 +266,6 @@ module HTTPX
278
266
  when :idle
279
267
  transition(:connecting)
280
268
  when :connected
281
- if @proxy_pending
282
- while (req = @proxy_pendind.shift)
283
- send(req)
284
- end
285
- end
286
-
287
269
  transition(:open)
288
270
  end
289
271
  end
@@ -9,7 +9,7 @@ module HTTPX
9
9
  # * when the server is unavailable (503);
10
10
  # * when a 3xx request comes with a "retry-after" value
11
11
  #
12
- # https://gitlab.com/os85/httpx/wikis/RateLimiter
12
+ # https://gitlab.com/os85/httpx/wikis/Rate-Limiter
13
13
  #
14
14
  module RateLimiter
15
15
  class << self
@@ -132,6 +132,10 @@ module HTTPX
132
132
  RETRYABLE_ERRORS.any? { |klass| ex.is_a?(klass) }
133
133
  end
134
134
 
135
+ def proxy_error?(request, response)
136
+ super && !request.retries.positive?
137
+ end
138
+
135
139
  #
136
140
  # Atttempt to set the request to perform a partial range request.
137
141
  # This happens if the peer server accepts byte-range requests, and
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ class ServerSideRequestForgeryError < Error; end
5
+
6
+ module Plugins
7
+ #
8
+ # This plugin adds support for preventing Server-Side Request Forgery attacks.
9
+ #
10
+ # https://gitlab.com/os85/httpx/wikis/Server-Side-Request-Forgery-Filter
11
+ #
12
+ module SsrfFilter
13
+ module IPAddrExtensions
14
+ refine IPAddr do
15
+ def prefixlen
16
+ mask_addr = @mask_addr
17
+ raise "Invalid mask" if mask_addr.zero?
18
+
19
+ mask_addr >>= 1 while (mask_addr & 0x1).zero?
20
+
21
+ length = 0
22
+ while mask_addr & 0x1 == 0x1
23
+ length += 1
24
+ mask_addr >>= 1
25
+ end
26
+
27
+ length
28
+ end
29
+ end
30
+ end
31
+
32
+ using IPAddrExtensions
33
+
34
+ # https://en.wikipedia.org/wiki/Reserved_IP_addresses
35
+ IPV4_BLACKLIST = [
36
+ IPAddr.new("0.0.0.0/8"), # Current network (only valid as source address)
37
+ IPAddr.new("10.0.0.0/8"), # Private network
38
+ IPAddr.new("100.64.0.0/10"), # Shared Address Space
39
+ IPAddr.new("127.0.0.0/8"), # Loopback
40
+ IPAddr.new("169.254.0.0/16"), # Link-local
41
+ IPAddr.new("172.16.0.0/12"), # Private network
42
+ IPAddr.new("192.0.0.0/24"), # IETF Protocol Assignments
43
+ IPAddr.new("192.0.2.0/24"), # TEST-NET-1, documentation and examples
44
+ IPAddr.new("192.88.99.0/24"), # IPv6 to IPv4 relay (includes 2002::/16)
45
+ IPAddr.new("192.168.0.0/16"), # Private network
46
+ IPAddr.new("198.18.0.0/15"), # Network benchmark tests
47
+ IPAddr.new("198.51.100.0/24"), # TEST-NET-2, documentation and examples
48
+ IPAddr.new("203.0.113.0/24"), # TEST-NET-3, documentation and examples
49
+ IPAddr.new("224.0.0.0/4"), # IP multicast (former Class D network)
50
+ IPAddr.new("240.0.0.0/4"), # Reserved (former Class E network)
51
+ IPAddr.new("255.255.255.255"), # Broadcast
52
+ ].freeze
53
+
54
+ IPV6_BLACKLIST = ([
55
+ IPAddr.new("::1/128"), # Loopback
56
+ IPAddr.new("64:ff9b::/96"), # IPv4/IPv6 translation (RFC 6052)
57
+ IPAddr.new("100::/64"), # Discard prefix (RFC 6666)
58
+ IPAddr.new("2001::/32"), # Teredo tunneling
59
+ IPAddr.new("2001:10::/28"), # Deprecated (previously ORCHID)
60
+ IPAddr.new("2001:20::/28"), # ORCHIDv2
61
+ IPAddr.new("2001:db8::/32"), # Addresses used in documentation and example source code
62
+ IPAddr.new("2002::/16"), # 6to4
63
+ IPAddr.new("fc00::/7"), # Unique local address
64
+ IPAddr.new("fe80::/10"), # Link-local address
65
+ IPAddr.new("ff00::/8"), # Multicast
66
+ ] + IPV4_BLACKLIST.flat_map do |ipaddr|
67
+ prefixlen = ipaddr.prefixlen
68
+
69
+ ipv4_compatible = ipaddr.ipv4_compat.mask(96 + prefixlen)
70
+ ipv4_mapped = ipaddr.ipv4_mapped.mask(80 + prefixlen)
71
+
72
+ [ipv4_compatible, ipv4_mapped]
73
+ end).freeze
74
+
75
+ class << self
76
+ def extra_options(options)
77
+ options.merge(allowed_schemes: %w[https http])
78
+ end
79
+
80
+ def unsafe_ip_address?(ipaddr)
81
+ range = ipaddr.to_range
82
+ return true if range.first != range.last
83
+
84
+ return IPV6_BLACKLIST.any? { |r| r.include?(ipaddr) } if ipaddr.ipv6?
85
+
86
+ IPV4_BLACKLIST.any? { |r| r.include?(ipaddr) } # then it's IPv4
87
+ end
88
+ end
89
+
90
+ module OptionsMethods
91
+ def option_allowed_schemes(value)
92
+ Array(value)
93
+ end
94
+ end
95
+
96
+ module InstanceMethods
97
+ def send_requests(*requests)
98
+ responses = requests.map do |request|
99
+ next if @options.allowed_schemes.include?(request.uri.scheme)
100
+
101
+ error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
102
+ error.set_backtrace(caller)
103
+ response = ErrorResponse.new(request, error, request.options)
104
+ request.emit(:response, response)
105
+ response
106
+ end
107
+ allowed_requests = requests.select { |req| responses[requests.index(req)].nil? }
108
+ allowed_responses = super(*allowed_requests)
109
+ allowed_responses.each_with_index do |res, idx|
110
+ req = allowed_requests[idx]
111
+ responses[requests.index(req)] = res
112
+ end
113
+
114
+ responses
115
+ end
116
+ end
117
+
118
+ module ConnectionMethods
119
+ def initialize(*)
120
+ begin
121
+ super
122
+ rescue ServerSideRequestForgeryError => e
123
+ # may raise when IPs are passed as options via :addresses
124
+ throw(:resolve_error, e)
125
+ end
126
+ end
127
+
128
+ def addresses=(addrs)
129
+ addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
130
+
131
+ addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
132
+
133
+ raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
134
+
135
+ super
136
+ end
137
+ end
138
+ end
139
+
140
+ register_plugin :ssrf_filter, SsrfFilter
141
+ end
142
+ end
@@ -5,6 +5,7 @@ module HTTPX
5
5
  def initialize(request, session)
6
6
  @request = request
7
7
  @session = session
8
+ @response = nil
8
9
  end
9
10
 
10
11
  def each(&block)
@@ -25,7 +26,7 @@ module HTTPX
25
26
  def each_line
26
27
  return enum_for(__method__) unless block_given?
27
28
 
28
- line = +""
29
+ line = "".b
29
30
 
30
31
  each do |chunk|
31
32
  line << chunk
@@ -36,6 +37,8 @@ module HTTPX
36
37
  line = line.byteslice(idx + 1..-1)
37
38
  end
38
39
  end
40
+
41
+ yield line unless line.empty?
39
42
  end
40
43
 
41
44
  # This is a ghost method. It's to be used ONLY internally, when processing streams
@@ -58,8 +61,10 @@ module HTTPX
58
61
  private
59
62
 
60
63
  def response
61
- @response ||= begin
62
- @request.response || @session.request(@request)
64
+ return @response if @response
65
+
66
+ @request.response || begin
67
+ @response = @session.request(@request)
63
68
  end
64
69
  end
65
70
 
@@ -78,7 +83,7 @@ module HTTPX
78
83
  #
79
84
  # This plugin adds support for stream response (text/event-stream).
80
85
  #
81
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Stream
86
+ # https://gitlab.com/os85/httpx/wikis/Stream
82
87
  #
83
88
  module Stream
84
89
  def self.extra_options(options)
@@ -6,7 +6,7 @@ module HTTPX
6
6
  # This plugin adds support for upgrading an HTTP/1.1 connection to HTTP/2
7
7
  # via an Upgrade: h2 response declaration
8
8
  #
9
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Upgrade#h2
9
+ # https://gitlab.com/os85/httpx/wikis/Connection-Upgrade#h2
10
10
  #
11
11
  module H2
12
12
  class << self
@@ -6,7 +6,7 @@ module HTTPX
6
6
  # This plugin helps negotiating a new protocol from an HTTP/1.1 connection, via the
7
7
  # Upgrade header.
8
8
  #
9
- # https://gitlab.com/honeyryderchuck/httpx/wikis/Upgrade
9
+ # https://gitlab.com/os85/httpx/wikis/Upgrade
10
10
  #
11
11
  module Upgrade
12
12
  class << self
@@ -5,7 +5,7 @@ module HTTPX
5
5
  #
6
6
  # This plugin implements convenience methods for performing WEBDAV requests.
7
7
  #
8
- # https://gitlab.com/honeyryderchuck/httpx/wikis/WEBDAV
8
+ # https://gitlab.com/os85/httpx/wikis/WebDav
9
9
  #
10
10
  module WebDav
11
11
  module InstanceMethods
data/lib/httpx/pool.rb CHANGED
@@ -17,8 +17,6 @@ module HTTPX
17
17
  @timers = Timers.new
18
18
  @selector = Selector.new
19
19
  @connections = []
20
- @eden_connections = []
21
- @connected_connections = 0
22
20
  end
23
21
 
24
22
  def empty?
@@ -45,16 +43,15 @@ module HTTPX
45
43
  connection.emit(:error, e)
46
44
  end
47
45
  rescue Exception # rubocop:disable Lint/RescueException
48
- @connections.each(&:reset)
46
+ @connections.each(&:force_reset)
49
47
  raise
50
48
  end
51
49
 
52
50
  def close(connections = @connections)
53
51
  return if connections.empty?
54
52
 
55
- @eden_connections.clear
56
53
  connections = connections.reject(&:inflight?)
57
- connections.each(&:close)
54
+ connections.each(&:terminate)
58
55
  next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
59
56
 
60
57
  # close resolvers
@@ -68,22 +65,36 @@ module HTTPX
68
65
  resolver.close unless resolver.closed?
69
66
  end
70
67
  # for https resolver
71
- resolver_connections.each(&:close)
68
+ resolver_connections.each(&:terminate)
72
69
  next_tick until resolver_connections.none? { |c| c.state != :idle && @connections.include?(c) }
73
70
  end
74
71
 
75
72
  def init_connection(connection, _options)
76
- resolve_connection(connection) unless connection.family
77
73
  connection.timers = @timers
78
- connection.on(:open) do
79
- @connected_connections += 1
80
- end
81
74
  connection.on(:activate) do
82
75
  select_connection(connection)
83
76
  end
77
+ connection.on(:exhausted) do
78
+ case connection.state
79
+ when :closed
80
+ connection.idling
81
+ @connections << connection
82
+ select_connection(connection)
83
+ when :closing
84
+ connection.once(:close) do
85
+ connection.idling
86
+ @connections << connection
87
+ select_connection(connection)
88
+ end
89
+ end
90
+ end
84
91
  connection.on(:close) do
85
92
  unregister_connection(connection)
86
93
  end
94
+ connection.on(:terminate) do
95
+ unregister_connection(connection, true)
96
+ end
97
+ resolve_connection(connection) unless connection.family
87
98
  end
88
99
 
89
100
  def deactivate(connections)
@@ -102,16 +113,15 @@ module HTTPX
102
113
  connection.match?(uri, options)
103
114
  end
104
115
 
105
- unless conn
106
- @eden_connections.delete_if do |connection|
107
- is_expired = connection.expired?
108
- conn = connection if conn.nil? && !is_expired && connection.match?(uri, options)
109
- is_expired
110
- end
116
+ return unless conn
111
117
 
112
- if conn
118
+ case conn.state
119
+ when :closed
120
+ conn.idling
121
+ select_connection(conn)
122
+ when :closing
123
+ conn.once(:close) do
113
124
  conn.idling
114
- @connections << conn
115
125
  select_connection(conn)
116
126
  end
117
127
  end
@@ -151,7 +161,7 @@ module HTTPX
151
161
 
152
162
  return connection if connection.family == family
153
163
 
154
- new_connection = connection.class.new(connection.type, connection.origin, connection.options)
164
+ new_connection = connection.class.new(connection.origin, connection.options)
155
165
  new_connection.family = family
156
166
 
157
167
  connection.once(:tcp_open) { new_connection.force_reset }
@@ -218,18 +228,12 @@ module HTTPX
218
228
  end
219
229
 
220
230
  def register_connection(connection)
221
- if connection.state == :open
222
- # if open, an IO was passed upstream, therefore
223
- # consider it connected already.
224
- @connected_connections += 1
225
- end
226
231
  select_connection(connection)
227
232
  end
228
233
 
229
- def unregister_connection(connection)
230
- @connections.delete(connection)
231
- @eden_connections << connection if connection.used? && !@eden_connections.include?(connection)
232
- @connected_connections -= 1 if deselect_connection(connection)
234
+ def unregister_connection(connection, cleanup = !connection.used?)
235
+ @connections.delete(connection) if cleanup
236
+ deselect_connection(connection)
233
237
  end
234
238
 
235
239
  def select_connection(connection)
@@ -70,9 +70,9 @@ module HTTPX
70
70
 
71
71
  # sets the body to yield using chunked trannsfer encoding format.
72
72
  def stream(body)
73
- encoded = body
74
- encoded = Transcoder::Chunker.encode(body.enum_for(:each)) if chunked?
75
- encoded
73
+ return body unless chunked?
74
+
75
+ Transcoder::Chunker.encode(body.enum_for(:each))
76
76
  end
77
77
 
78
78
  # returns whether the body yields infinitely.
data/lib/httpx/request.rb CHANGED
@@ -119,10 +119,19 @@ module HTTPX
119
119
  def response=(response)
120
120
  return unless response
121
121
 
122
- if response.is_a?(Response) && response.status == 100 && @headers.key?("expect")
123
- @informational_status = response.status
124
- return
122
+ if response.is_a?(Response) && response.status < 200
123
+ # deal with informational responses
124
+
125
+ if response.status == 100 && @headers.key?("expect")
126
+ @informational_status = response.status
127
+ return
128
+ end
129
+
130
+ # 103 Early Hints advertises resources in document to browsers.
131
+ # not very relevant for an HTTP client, discard.
132
+ return if response.status >= 103
125
133
  end
134
+
126
135
  @response = response
127
136
 
128
137
  emit(:response_started, response)
@@ -27,7 +27,7 @@ module HTTPX
27
27
  use_get: false,
28
28
  }.freeze
29
29
 
30
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
30
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
31
31
 
32
32
  def initialize(_, options)
33
33
  super
@@ -50,6 +50,7 @@ module HTTPX
50
50
  if @uri_addresses.empty?
51
51
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
52
52
  ex.set_backtrace(caller)
53
+ connection.force_reset
53
54
  throw(:resolve_error, ex)
54
55
  end
55
56
 
@@ -67,7 +68,7 @@ module HTTPX
67
68
  def resolver_connection
68
69
  @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
69
70
  @building_connection = true
70
- connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
71
+ connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
71
72
  @pool.init_connection(connection, @options)
72
73
  # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
73
74
  emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
@@ -88,6 +88,7 @@ module HTTPX
88
88
  if @nameserver.nil?
89
89
  ex = ResolveError.new("No available nameserver")
90
90
  ex.set_backtrace(caller)
91
+ connection.force_reset
91
92
  throw(:resolve_error, ex)
92
93
  else
93
94
  @connections << connection
@@ -38,6 +38,8 @@ module HTTPX
38
38
 
39
39
  def close; end
40
40
 
41
+ alias_method :terminate, :close
42
+
41
43
  def closed?
42
44
  true
43
45
  end
@@ -65,20 +67,29 @@ module HTTPX
65
67
  unless connection.state == :closed ||
66
68
  # double emission check
67
69
  (connection.addresses && addresses.intersect?(connection.addresses))
68
- emit_resolved_connection(connection, addresses)
70
+ emit_resolved_connection(connection, addresses, early_resolve)
69
71
  end
70
72
  end
71
73
  else
72
- emit_resolved_connection(connection, addresses)
74
+ emit_resolved_connection(connection, addresses, early_resolve)
73
75
  end
74
76
  end
75
77
 
76
78
  private
77
79
 
78
- def emit_resolved_connection(connection, addresses)
79
- connection.addresses = addresses
80
-
81
- emit(:resolve, connection)
80
+ def emit_resolved_connection(connection, addresses, early_resolve)
81
+ begin
82
+ connection.addresses = addresses
83
+
84
+ emit(:resolve, connection)
85
+ rescue StandardError => e
86
+ if early_resolve
87
+ connection.force_reset
88
+ throw(:resolve_error, e)
89
+ else
90
+ emit(:error, connection, e)
91
+ end
92
+ end
82
93
  end
83
94
 
84
95
  def early_resolve(connection, hostname: connection.origin.host)
@@ -264,4 +264,4 @@ end
264
264
 
265
265
  require_relative "response/body"
266
266
  require_relative "response/buffer"
267
- require_relative "pmatch_extensions" if RUBY_VERSION >= "3.0.0"
267
+ require_relative "pmatch_extensions" if RUBY_VERSION >= "2.7.0"