httpx 1.1.4 → 1.2.0

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 (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"