httpx 0.21.0 → 1.2.1

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 (229) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +0 -48
  3. data/README.md +54 -45
  4. data/doc/release_notes/0_10_0.md +2 -2
  5. data/doc/release_notes/0_11_0.md +3 -5
  6. data/doc/release_notes/0_12_0.md +5 -5
  7. data/doc/release_notes/0_13_0.md +4 -4
  8. data/doc/release_notes/0_14_0.md +2 -2
  9. data/doc/release_notes/0_16_0.md +3 -3
  10. data/doc/release_notes/0_17_0.md +1 -1
  11. data/doc/release_notes/0_18_0.md +4 -4
  12. data/doc/release_notes/0_18_2.md +1 -1
  13. data/doc/release_notes/0_19_0.md +1 -1
  14. data/doc/release_notes/0_20_0.md +1 -1
  15. data/doc/release_notes/0_21_0.md +7 -5
  16. data/doc/release_notes/0_21_1.md +12 -0
  17. data/doc/release_notes/0_22_0.md +13 -0
  18. data/doc/release_notes/0_22_1.md +11 -0
  19. data/doc/release_notes/0_22_2.md +5 -0
  20. data/doc/release_notes/0_22_3.md +55 -0
  21. data/doc/release_notes/0_22_4.md +6 -0
  22. data/doc/release_notes/0_22_5.md +6 -0
  23. data/doc/release_notes/0_23_0.md +42 -0
  24. data/doc/release_notes/0_23_1.md +5 -0
  25. data/doc/release_notes/0_23_2.md +5 -0
  26. data/doc/release_notes/0_23_3.md +6 -0
  27. data/doc/release_notes/0_23_4.md +5 -0
  28. data/doc/release_notes/0_24_0.md +48 -0
  29. data/doc/release_notes/0_24_1.md +12 -0
  30. data/doc/release_notes/0_24_2.md +12 -0
  31. data/doc/release_notes/0_24_3.md +12 -0
  32. data/doc/release_notes/0_24_4.md +18 -0
  33. data/doc/release_notes/0_24_5.md +6 -0
  34. data/doc/release_notes/0_24_6.md +5 -0
  35. data/doc/release_notes/0_24_7.md +10 -0
  36. data/doc/release_notes/1_0_0.md +60 -0
  37. data/doc/release_notes/1_0_1.md +5 -0
  38. data/doc/release_notes/1_0_2.md +7 -0
  39. data/doc/release_notes/1_1_0.md +32 -0
  40. data/doc/release_notes/1_1_1.md +17 -0
  41. data/doc/release_notes/1_1_2.md +12 -0
  42. data/doc/release_notes/1_1_3.md +18 -0
  43. data/doc/release_notes/1_1_4.md +6 -0
  44. data/doc/release_notes/1_1_5.md +12 -0
  45. data/doc/release_notes/1_2_0.md +49 -0
  46. data/doc/release_notes/1_2_1.md +6 -0
  47. data/lib/httpx/adapters/datadog.rb +100 -106
  48. data/lib/httpx/adapters/faraday.rb +143 -107
  49. data/lib/httpx/adapters/sentry.rb +26 -7
  50. data/lib/httpx/adapters/webmock.rb +33 -17
  51. data/lib/httpx/altsvc.rb +61 -24
  52. data/lib/httpx/base64.rb +27 -0
  53. data/lib/httpx/buffer.rb +12 -0
  54. data/lib/httpx/callbacks.rb +5 -3
  55. data/lib/httpx/chainable.rb +54 -39
  56. data/lib/httpx/connection/http1.rb +62 -37
  57. data/lib/httpx/connection/http2.rb +16 -27
  58. data/lib/httpx/connection.rb +213 -120
  59. data/lib/httpx/domain_name.rb +10 -13
  60. data/lib/httpx/errors.rb +34 -2
  61. data/lib/httpx/extensions.rb +4 -134
  62. data/lib/httpx/io/ssl.rb +77 -71
  63. data/lib/httpx/io/tcp.rb +46 -70
  64. data/lib/httpx/io/udp.rb +18 -52
  65. data/lib/httpx/io/unix.rb +6 -13
  66. data/lib/httpx/io.rb +3 -9
  67. data/lib/httpx/loggable.rb +4 -19
  68. data/lib/httpx/options.rb +168 -110
  69. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  70. data/lib/httpx/plugins/{authentication → auth}/digest.rb +13 -14
  71. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  72. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  73. data/lib/httpx/plugins/auth.rb +25 -0
  74. data/lib/httpx/plugins/aws_sdk_authentication.rb +1 -3
  75. data/lib/httpx/plugins/aws_sigv4.rb +5 -6
  76. data/lib/httpx/plugins/basic_auth.rb +29 -0
  77. data/lib/httpx/plugins/brotli.rb +50 -0
  78. data/lib/httpx/plugins/callbacks.rb +91 -0
  79. data/lib/httpx/plugins/circuit_breaker/circuit.rb +40 -16
  80. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +14 -5
  81. data/lib/httpx/plugins/circuit_breaker.rb +30 -7
  82. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  83. data/lib/httpx/plugins/cookies.rb +20 -10
  84. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +11 -12
  85. data/lib/httpx/plugins/expect.rb +15 -13
  86. data/lib/httpx/plugins/follow_redirects.rb +71 -29
  87. data/lib/httpx/plugins/grpc/call.rb +2 -3
  88. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  89. data/lib/httpx/plugins/grpc/message.rb +7 -37
  90. data/lib/httpx/plugins/grpc.rb +35 -29
  91. data/lib/httpx/plugins/h2c.rb +25 -18
  92. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  93. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  94. data/lib/httpx/plugins/oauth.rb +170 -0
  95. data/lib/httpx/plugins/persistent.rb +1 -1
  96. data/lib/httpx/plugins/proxy/http.rb +15 -10
  97. data/lib/httpx/plugins/proxy/socks4.rb +8 -6
  98. data/lib/httpx/plugins/proxy/socks5.rb +10 -8
  99. data/lib/httpx/plugins/proxy.rb +69 -67
  100. data/lib/httpx/plugins/push_promise.rb +1 -1
  101. data/lib/httpx/plugins/rate_limiter.rb +3 -1
  102. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  103. data/lib/httpx/plugins/response_cache/store.rb +34 -17
  104. data/lib/httpx/plugins/response_cache.rb +6 -6
  105. data/lib/httpx/plugins/retries.rb +61 -12
  106. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  107. data/lib/httpx/plugins/stream.rb +27 -32
  108. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  109. data/lib/httpx/plugins/upgrade.rb +8 -10
  110. data/lib/httpx/plugins/webdav.rb +10 -8
  111. data/lib/httpx/pool.rb +85 -23
  112. data/lib/httpx/punycode.rb +9 -291
  113. data/lib/httpx/request/body.rb +158 -0
  114. data/lib/httpx/request.rb +86 -121
  115. data/lib/httpx/resolver/https.rb +54 -17
  116. data/lib/httpx/resolver/multi.rb +8 -12
  117. data/lib/httpx/resolver/native.rb +163 -70
  118. data/lib/httpx/resolver/resolver.rb +28 -13
  119. data/lib/httpx/resolver/system.rb +15 -10
  120. data/lib/httpx/resolver.rb +38 -16
  121. data/lib/httpx/response/body.rb +242 -0
  122. data/lib/httpx/response/buffer.rb +96 -0
  123. data/lib/httpx/response.rb +113 -211
  124. data/lib/httpx/selector.rb +2 -4
  125. data/lib/httpx/session.rb +91 -64
  126. data/lib/httpx/session_extensions.rb +4 -1
  127. data/lib/httpx/timers.rb +28 -8
  128. data/lib/httpx/transcoder/body.rb +0 -2
  129. data/lib/httpx/transcoder/chunker.rb +0 -1
  130. data/lib/httpx/transcoder/deflate.rb +37 -0
  131. data/lib/httpx/transcoder/form.rb +52 -33
  132. data/lib/httpx/transcoder/gzip.rb +74 -0
  133. data/lib/httpx/transcoder/json.rb +2 -5
  134. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  135. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +3 -3
  136. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  137. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  138. data/lib/httpx/transcoder/multipart.rb +17 -0
  139. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  140. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  141. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  142. data/lib/httpx/transcoder/xml.rb +0 -5
  143. data/lib/httpx/transcoder.rb +4 -6
  144. data/lib/httpx/utils.rb +36 -16
  145. data/lib/httpx/version.rb +1 -1
  146. data/lib/httpx.rb +12 -14
  147. data/sig/altsvc.rbs +33 -0
  148. data/sig/buffer.rbs +1 -0
  149. data/sig/callbacks.rbs +3 -3
  150. data/sig/chainable.rbs +10 -9
  151. data/sig/connection/http1.rbs +5 -4
  152. data/sig/connection/http2.rbs +1 -1
  153. data/sig/connection.rbs +46 -24
  154. data/sig/errors.rbs +9 -3
  155. data/sig/httpx.rbs +5 -4
  156. data/sig/io/ssl.rbs +26 -0
  157. data/sig/io/tcp.rbs +60 -0
  158. data/sig/io/udp.rbs +20 -0
  159. data/sig/io/unix.rbs +10 -0
  160. data/sig/options.rbs +28 -12
  161. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  162. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  163. data/sig/plugins/auth.rbs +13 -0
  164. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  165. data/sig/plugins/brotli.rbs +22 -0
  166. data/sig/plugins/callbacks.rbs +38 -0
  167. data/sig/plugins/circuit_breaker.rbs +13 -3
  168. data/sig/plugins/compression.rbs +6 -4
  169. data/sig/plugins/cookies/jar.rbs +2 -2
  170. data/sig/plugins/cookies.rbs +2 -0
  171. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  172. data/sig/plugins/follow_redirects.rbs +11 -2
  173. data/sig/plugins/grpc/call.rbs +19 -0
  174. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  175. data/sig/plugins/grpc/message.rbs +17 -0
  176. data/sig/plugins/grpc.rbs +2 -32
  177. data/sig/plugins/h2c.rbs +1 -1
  178. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  179. data/sig/plugins/oauth.rbs +54 -0
  180. data/sig/plugins/proxy/socks4.rbs +4 -4
  181. data/sig/plugins/proxy/socks5.rbs +2 -2
  182. data/sig/plugins/proxy/ssh.rbs +1 -1
  183. data/sig/plugins/proxy.rbs +10 -4
  184. data/sig/plugins/response_cache.rbs +12 -3
  185. data/sig/plugins/retries.rbs +28 -8
  186. data/sig/plugins/stream.rbs +24 -17
  187. data/sig/plugins/upgrade.rbs +5 -3
  188. data/sig/pool.rbs +5 -4
  189. data/sig/request/body.rbs +40 -0
  190. data/sig/request.rbs +12 -28
  191. data/sig/resolver/https.rbs +7 -2
  192. data/sig/resolver/native.rbs +10 -4
  193. data/sig/resolver/resolver.rbs +6 -4
  194. data/sig/resolver/system.rbs +2 -0
  195. data/sig/resolver.rbs +9 -5
  196. data/sig/response/body.rbs +53 -0
  197. data/sig/response/buffer.rbs +24 -0
  198. data/sig/response.rbs +17 -38
  199. data/sig/session.rbs +24 -18
  200. data/sig/timers.rbs +17 -7
  201. data/sig/transcoder/body.rbs +4 -3
  202. data/sig/transcoder/deflate.rbs +11 -0
  203. data/sig/transcoder/form.rbs +5 -3
  204. data/sig/transcoder/gzip.rbs +24 -0
  205. data/sig/transcoder/json.rbs +4 -2
  206. data/sig/{plugins → transcoder}/multipart.rbs +3 -12
  207. data/sig/transcoder/utils/body_reader.rbs +15 -0
  208. data/sig/transcoder/utils/deflater.rbs +29 -0
  209. data/sig/transcoder/utils/inflater.rbs +12 -0
  210. data/sig/transcoder/xml.rbs +1 -1
  211. data/sig/transcoder.rbs +22 -7
  212. data/sig/utils.rbs +2 -0
  213. metadata +127 -40
  214. data/lib/httpx/plugins/authentication.rb +0 -20
  215. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  216. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  217. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  218. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  219. data/lib/httpx/plugins/compression.rb +0 -164
  220. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  221. data/lib/httpx/plugins/multipart.rb +0 -84
  222. data/lib/httpx/registry.rb +0 -85
  223. data/sig/plugins/authentication.rbs +0 -11
  224. data/sig/plugins/compression/brotli.rbs +0 -21
  225. data/sig/plugins/compression/deflate.rbs +0 -17
  226. data/sig/plugins/compression/gzip.rbs +0 -29
  227. data/sig/registry.rbs +0 -13
  228. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  229. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -29,41 +29,39 @@ module HTTPX
29
29
  #
30
30
  class Connection
31
31
  extend Forwardable
32
- include Registry
33
32
  include Loggable
34
33
  include Callbacks
35
34
 
36
35
  using URIExtensions
37
- using NumericExtensions
38
36
 
39
37
  require "httpx/connection/http2"
40
38
  require "httpx/connection/http1"
41
39
 
42
- BUFFER_SIZE = 1 << 14
43
-
44
40
  def_delegator :@io, :closed?
45
41
 
46
42
  def_delegator :@write_buffer, :empty?
47
43
 
48
- attr_reader :io, :origin, :origins, :state, :pending, :options
44
+ attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session
49
45
 
50
46
  attr_writer :timers
51
47
 
52
- def initialize(type, uri, options)
53
- @type = type
48
+ attr_accessor :family
49
+
50
+ def initialize(uri, options)
54
51
  @origins = [uri.origin]
55
52
  @origin = Utils.to_uri(uri.origin)
56
53
  @options = Options.new(options)
54
+ @type = initialize_type(uri, @options)
57
55
  @window_size = @options.window_size
58
- @read_buffer = Buffer.new(BUFFER_SIZE)
59
- @write_buffer = Buffer.new(BUFFER_SIZE)
56
+ @read_buffer = Buffer.new(@options.buffer_size)
57
+ @write_buffer = Buffer.new(@options.buffer_size)
60
58
  @pending = []
61
59
  on(:error, &method(:on_error))
62
60
  if @options.io
63
61
  # if there's an already open IO, get its
64
62
  # peer address, and force-initiate the parser
65
63
  transition(:already_open)
66
- @io = IO.registry(@type).new(@origin, nil, @options)
64
+ @io = build_socket
67
65
  parser
68
66
  else
69
67
  transition(:idle)
@@ -71,7 +69,8 @@ module HTTPX
71
69
 
72
70
  @inflight = 0
73
71
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
74
- @total_timeout = @options.timeout[:total_timeout]
72
+
73
+ @intervals = []
75
74
 
76
75
  self.addresses = @options.addresses if @options.addresses
77
76
  end
@@ -82,7 +81,7 @@ module HTTPX
82
81
  if @io
83
82
  @io.add_addresses(addrs)
84
83
  else
85
- @io = IO.registry(@type).new(@origin, addrs, @options)
84
+ @io = build_socket(addrs)
86
85
  end
87
86
  end
88
87
 
@@ -91,30 +90,33 @@ module HTTPX
91
90
  end
92
91
 
93
92
  def match?(uri, options)
94
- return false if @state == :closing || @state == :closed
95
-
96
- return false if exhausted?
93
+ return false if !used? && (@state == :closing || @state == :closed)
97
94
 
98
95
  (
99
- (
100
- @origins.include?(uri.origin) &&
101
- # if there is more than one origin to match, it means that this connection
102
- # was the result of coalescing. To prevent blind trust in the case where the
103
- # origin came from an ORIGIN frame, we're going to verify the hostname with the
104
- # SSL certificate
105
- (@origins.size == 1 || @origin == uri.origin || (@io && @io.verify_hostname(uri.host)))
106
- ) && @options == options
107
- ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
96
+ @origins.include?(uri.origin) &&
97
+ # if there is more than one origin to match, it means that this connection
98
+ # was the result of coalescing. To prevent blind trust in the case where the
99
+ # origin came from an ORIGIN frame, we're going to verify the hostname with the
100
+ # SSL certificate
101
+ (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
102
+ ) && @options == options
103
+ end
104
+
105
+ def expired?
106
+ return false unless @io
107
+
108
+ @io.expired?
108
109
  end
109
110
 
110
111
  def mergeable?(connection)
111
112
  return false if @state == :closing || @state == :closed || !@io
112
113
 
113
- return false if exhausted?
114
-
115
114
  return false unless connection.addresses
116
115
 
117
- !(@io.addresses & connection.addresses).empty? && @options == connection.options
116
+ (
117
+ (open? && @origin == connection.origin) ||
118
+ !(@io.addresses & (connection.addresses || [])).empty?
119
+ ) && @options == connection.options
118
120
  end
119
121
 
120
122
  # coalescable connections need to be mergeable!
@@ -131,11 +133,17 @@ module HTTPX
131
133
  end
132
134
 
133
135
  def create_idle(options = {})
134
- self.class.new(@type, @origin, @options.merge(options))
136
+ self.class.new(@origin, @options.merge(options))
135
137
  end
136
138
 
137
139
  def merge(connection)
138
140
  @origins |= connection.instance_variable_get(:@origins)
141
+ if connection.ssl_session
142
+ @ssl_session = connection.ssl_session
143
+ @io.session_new_cb do |sess|
144
+ @ssl_session = sess
145
+ end if @io
146
+ end
139
147
  connection.purge_pending do |req|
140
148
  send(req)
141
149
  end
@@ -153,24 +161,6 @@ module HTTPX
153
161
  end
154
162
  end
155
163
 
156
- # checks if this is connection is an alternative service of
157
- # +uri+
158
- def match_altsvcs?(uri)
159
- @origins.any? { |origin| uri.altsvc_match?(origin) } ||
160
- AltSvc.cached_altsvc(@origin).any? do |altsvc|
161
- origin = altsvc["origin"]
162
- origin.altsvc_match?(uri.origin)
163
- end
164
- end
165
-
166
- def match_altsvc_options?(uri, options)
167
- return @options == options unless @options.ssl[:hostname] == uri.host
168
-
169
- dup_options = @options.merge(ssl: { hostname: nil })
170
- dup_options.ssl.delete(:hostname)
171
- dup_options == options
172
- end
173
-
174
164
  def connecting?
175
165
  @state == :idle
176
166
  end
@@ -201,12 +191,14 @@ module HTTPX
201
191
 
202
192
  def call
203
193
  case @state
194
+ when :idle
195
+ connect
196
+ consume
204
197
  when :closed
205
198
  return
206
199
  when :closing
207
200
  consume
208
201
  transition(:closed)
209
- emit(:close)
210
202
  when :open
211
203
  consume
212
204
  end
@@ -219,16 +211,29 @@ module HTTPX
219
211
  @parser.close if @parser
220
212
  end
221
213
 
214
+ def terminate
215
+ @connected_at = nil if @state == :closed
216
+
217
+ close
218
+ end
219
+
220
+ # bypasses the state machine to force closing of connections still connecting.
221
+ # **only** used for Happy Eyeballs v2.
222
+ def force_reset
223
+ @state = :closing
224
+ transition(:closed)
225
+ end
226
+
222
227
  def reset
228
+ return if @state == :closing || @state == :closed
229
+
223
230
  transition(:closing)
231
+
224
232
  transition(:closed)
225
- emit(:close)
226
233
  end
227
234
 
228
235
  def send(request)
229
236
  if @parser && !@write_buffer.full?
230
- request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
231
-
232
237
  if @response_received_at && @keep_alive_timeout &&
233
238
  Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
234
239
  # when pushing a request into an existing connection, we have to check whether there
@@ -248,28 +253,24 @@ module HTTPX
248
253
  end
249
254
 
250
255
  def timeout
251
- if @total_timeout
252
- return @total_timeout unless @connected_at
253
-
254
- elapsed_time = @total_timeout - Utils.elapsed_time(@connected_at)
255
-
256
- if elapsed_time.negative?
257
- ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
258
- ex.set_backtrace(caller)
259
- on_error(ex)
260
- return
261
- end
262
-
263
- return elapsed_time
264
- end
265
-
266
- return @timeout if defined?(@timeout)
256
+ return @timeout if @timeout
267
257
 
268
258
  return @options.timeout[:connect_timeout] if @state == :idle
269
259
 
270
260
  @options.timeout[:operation_timeout]
271
261
  end
272
262
 
263
+ def idling
264
+ purge_after_closed
265
+ @write_buffer.clear
266
+ transition(:idle)
267
+ @parser = nil if @parser
268
+ end
269
+
270
+ def used?
271
+ @connected_at
272
+ end
273
+
273
274
  def deactivate
274
275
  transition(:inactive)
275
276
  end
@@ -278,7 +279,15 @@ module HTTPX
278
279
  @state == :open || @state == :inactive
279
280
  end
280
281
 
281
- def raise_timeout_error(interval)
282
+ def handle_socket_timeout(interval)
283
+ @intervals.delete_if(&:elapsed?)
284
+
285
+ unless @intervals.empty?
286
+ # remove the intervals which will elapse
287
+
288
+ return
289
+ end
290
+
282
291
  error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
283
292
  error.set_backtrace(caller)
284
293
  on_error(error)
@@ -290,16 +299,15 @@ module HTTPX
290
299
  transition(:open)
291
300
  end
292
301
 
293
- def exhausted?
294
- @parser && parser.exhausted?
295
- end
296
-
297
302
  def consume
298
303
  return unless @io
299
304
 
300
305
  catch(:called) do
301
306
  epiped = false
302
307
  loop do
308
+ # connection may have
309
+ return if @state == :idle
310
+
303
311
  parser.consume
304
312
 
305
313
  # we exit if there's no more requests to process
@@ -309,7 +317,7 @@ module HTTPX
309
317
  # * the number of inflight requests
310
318
  # * the number of pending requests
311
319
  # * whether the write buffer has bytes (i.e. for close handshake)
312
- if @pending.size.zero? && @inflight.zero? && @write_buffer.empty?
320
+ if @pending.empty? && @inflight.zero? && @write_buffer.empty?
313
321
  log(level: 3) { "NO MORE REQUESTS..." }
314
322
  return
315
323
  end
@@ -353,7 +361,7 @@ module HTTPX
353
361
  break if @state == :closing || @state == :closed
354
362
 
355
363
  # exit #consume altogether if all outstanding requests have been dealt with
356
- return if @pending.size.zero? && @inflight.zero?
364
+ return if @pending.empty? && @inflight.zero?
357
365
  end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
358
366
 
359
367
  #
@@ -430,6 +438,7 @@ module HTTPX
430
438
 
431
439
  def send_request_to_parser(request)
432
440
  @inflight += 1
441
+ request.peer_address = @io.ip
433
442
  parser.send(request)
434
443
 
435
444
  set_request_timeouts(request)
@@ -440,7 +449,7 @@ module HTTPX
440
449
  end
441
450
 
442
451
  def build_parser(protocol = @io.protocol)
443
- parser = registry(protocol).new(@write_buffer, @options)
452
+ parser = self.class.parser_type(protocol).new(@write_buffer, @options)
444
453
  set_parser_callbacks(parser)
445
454
  parser
446
455
  end
@@ -464,33 +473,25 @@ module HTTPX
464
473
  request.emit(:promise, parser, stream)
465
474
  end
466
475
  parser.on(:exhausted) do
476
+ @pending.concat(parser.pending)
467
477
  emit(:exhausted)
468
478
  end
469
479
  parser.on(:origin) do |origin|
470
480
  @origins |= [origin]
471
481
  end
472
482
  parser.on(:close) do |force|
473
- transition(:closing)
474
- if force || @state == :idle
475
- transition(:closed)
476
- emit(:close)
483
+ if force
484
+ reset
485
+ emit(:terminate)
477
486
  end
478
487
  end
479
488
  parser.on(:close_handshake) do
480
489
  consume
481
490
  end
482
491
  parser.on(:reset) do
483
- if parser.empty?
484
- reset
485
- else
486
- transition(:closing)
487
- transition(:closed)
488
- emit(:reset)
489
-
490
- @parser.reset if @parser
491
- transition(:idle)
492
- transition(:open)
493
- end
492
+ @pending.concat(parser.pending) unless parser.empty?
493
+ reset
494
+ idling unless @pending.empty?
494
495
  end
495
496
  parser.on(:current_timeout) do
496
497
  @current_timeout = @timeout = parser.timeout
@@ -512,12 +513,26 @@ module HTTPX
512
513
 
513
514
  def transition(nextstate)
514
515
  handle_transition(nextstate)
515
- rescue Errno::ECONNREFUSED,
516
+ rescue Errno::ECONNABORTED,
517
+ Errno::ECONNREFUSED,
518
+ Errno::ECONNRESET,
516
519
  Errno::EADDRNOTAVAIL,
517
520
  Errno::EHOSTUNREACH,
518
- TLSError => e
521
+ Errno::EINVAL,
522
+ Errno::ENETUNREACH,
523
+ Errno::EPIPE,
524
+ Errno::ENOENT,
525
+ SocketError => e
526
+ # connect errors, exit gracefully
527
+ error = ConnectionError.new(e.message)
528
+ error.set_backtrace(e.backtrace)
529
+ connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
530
+ @state = :closed
531
+ emit(:close)
532
+ rescue TLSError => e
519
533
  # connect errors, exit gracefully
520
534
  handle_error(e)
535
+ connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
521
536
  @state = :closed
522
537
  emit(:close)
523
538
  end
@@ -527,10 +542,13 @@ module HTTPX
527
542
  when :idle
528
543
  @timeout = @current_timeout = @options.timeout[:connect_timeout]
529
544
 
545
+ @connected_at = nil
530
546
  when :open
531
547
  return if @state == :closed
532
548
 
533
549
  @io.connect
550
+ emit(:tcp_open, self) if @io.state == :connected
551
+
534
552
  return unless @io.connected?
535
553
 
536
554
  @connected_at = Utils.now
@@ -542,15 +560,28 @@ module HTTPX
542
560
  when :inactive
543
561
  return unless @state == :open
544
562
  when :closing
545
- return unless @state == :open
546
-
563
+ return unless @state == :idle || @state == :open
564
+
565
+ unless @write_buffer.empty?
566
+ # preset state before handshake, as error callbacks
567
+ # may take it back here.
568
+ @state = nextstate
569
+ # handshakes, try sending
570
+ consume
571
+ @write_buffer.clear
572
+ return
573
+ end
547
574
  when :closed
548
575
  return unless @state == :closing
549
576
  return unless @write_buffer.empty?
550
577
 
551
578
  purge_after_closed
579
+ emit(:close) if @pending.empty?
552
580
  when :already_open
553
581
  nextstate = :open
582
+ # the first check for given io readiness must still use a timeout.
583
+ # connect is the reasonable choice in such a case.
584
+ @timeout = @options.timeout[:connect_timeout]
554
585
  send_pending
555
586
  when :active
556
587
  return unless @state == :inactive
@@ -564,29 +595,55 @@ module HTTPX
564
595
  def purge_after_closed
565
596
  @io.close if @io
566
597
  @read_buffer.clear
567
- remove_instance_variable(:@timeout) if defined?(@timeout)
598
+ @timeout = nil
568
599
  end
569
600
 
570
- def on_error(error)
571
- if error.instance_of?(TimeoutError)
572
-
573
- if @total_timeout && @connected_at &&
574
- Utils.elapsed_time(@connected_at) > @total_timeout
575
- ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
576
- ex.set_backtrace(error.backtrace)
577
- error = ex
601
+ def initialize_type(uri, options)
602
+ options.transport || begin
603
+ case uri.scheme
604
+ when "http"
605
+ "tcp"
606
+ when "https"
607
+ "ssl"
578
608
  else
579
- # inactive connections do not contribute to the select loop, therefore
580
- # they should not fail due to such errors.
581
- return if @state == :inactive
609
+ raise UnsupportedSchemeError, "#{uri}: #{uri.scheme}: unsupported URI scheme"
610
+ end
611
+ end
612
+ end
582
613
 
583
- if @timeout
584
- @timeout -= error.timeout
585
- return unless @timeout <= 0
614
+ def build_socket(addrs = nil)
615
+ case @type
616
+ when "tcp"
617
+ TCP.new(@origin, addrs, @options)
618
+ when "ssl"
619
+ SSL.new(@origin, addrs, @options) do |sock|
620
+ sock.ssl_session = @ssl_session
621
+ sock.session_new_cb do |sess|
622
+ @ssl_session = sess
623
+
624
+ sock.ssl_session = sess
586
625
  end
626
+ end
627
+ when "unix"
628
+ UNIX.new(@origin, addrs, @options)
629
+ else
630
+ raise Error, "unsupported transport (#{@type})"
631
+ end
632
+ end
633
+
634
+ def on_error(error)
635
+ if error.instance_of?(TimeoutError)
636
+
637
+ # inactive connections do not contribute to the select loop, therefore
638
+ # they should not fail due to such errors.
639
+ return if @state == :inactive
587
640
 
588
- error = error.to_connection_error if connecting?
641
+ if @timeout
642
+ @timeout -= error.timeout
643
+ return unless @timeout <= 0
589
644
  end
645
+
646
+ error = error.to_connection_error if connecting?
590
647
  end
591
648
  handle_error(error)
592
649
  reset
@@ -603,19 +660,26 @@ module HTTPX
603
660
 
604
661
  def set_request_timeouts(request)
605
662
  write_timeout = request.write_timeout
606
- request.once(:headers) do
607
- @timers.after(write_timeout) { write_timeout_callback(request, write_timeout) }
608
- end unless write_timeout.nil? || write_timeout.infinite?
609
-
610
663
  read_timeout = request.read_timeout
611
- request.once(:done) do
612
- @timers.after(read_timeout) { read_timeout_callback(request, read_timeout) }
613
- end unless read_timeout.nil? || read_timeout.infinite?
614
-
615
664
  request_timeout = request.request_timeout
616
- request.once(:headers) do
617
- @timers.after(request_timeout) { read_timeout_callback(request, request_timeout, RequestTimeoutError) }
618
- end unless request_timeout.nil? || request_timeout.infinite?
665
+
666
+ unless write_timeout.nil? || write_timeout.infinite?
667
+ set_request_timeout(request, write_timeout, :headers, %i[done response]) do
668
+ write_timeout_callback(request, write_timeout)
669
+ end
670
+ end
671
+
672
+ unless read_timeout.nil? || read_timeout.infinite?
673
+ set_request_timeout(request, read_timeout, :done, :response) do
674
+ read_timeout_callback(request, read_timeout)
675
+ end
676
+ end
677
+
678
+ return if request_timeout.nil? || request_timeout.infinite?
679
+
680
+ set_request_timeout(request, request_timeout, :headers, :response) do
681
+ read_timeout_callback(request, request_timeout, RequestTimeoutError)
682
+ end
619
683
  end
620
684
 
621
685
  def write_timeout_callback(request, write_timeout)
@@ -635,5 +699,34 @@ module HTTPX
635
699
  error = error_type.new(request, request.response, read_timeout)
636
700
  on_error(error)
637
701
  end
702
+
703
+ def set_request_timeout(request, timeout, start_event, finish_events, &callback)
704
+ request.once(start_event) do
705
+ interval = @timers.after(timeout, callback)
706
+
707
+ Array(finish_events).each do |event|
708
+ # clean up request timeouts if the connection errors out
709
+ request.once(event) do
710
+ if @intervals.include?(interval)
711
+ interval.delete(callback)
712
+ @intervals.delete(interval) if interval.no_callbacks?
713
+ end
714
+ end
715
+ end
716
+
717
+ @intervals << interval
718
+ end
719
+ end
720
+
721
+ class << self
722
+ def parser_type(protocol)
723
+ case protocol
724
+ when "h2" then HTTP2
725
+ when "http/1.1" then HTTP1
726
+ else
727
+ raise Error, "unsupported protocol (##{protocol})"
728
+ end
729
+ end
730
+ end
638
731
  end
639
732
  end
@@ -51,8 +51,6 @@ module HTTPX
51
51
  # non-canonical domain.
52
52
  attr_reader :domain
53
53
 
54
- DOT = "." # :nodoc:
55
-
56
54
  class << self
57
55
  def new(domain)
58
56
  return domain if domain.is_a?(self)
@@ -63,8 +61,12 @@ module HTTPX
63
61
  # Normalizes a _domain_ using the Punycode algorithm as necessary.
64
62
  # The result will be a downcased, ASCII-only string.
65
63
  def normalize(domain)
66
- domain = domain.ascii_only? ? domain : domain.chomp(DOT).unicode_normalize(:nfc)
67
- Punycode.encode_hostname(domain).downcase
64
+ unless domain.ascii_only?
65
+ domain = domain.chomp(".").unicode_normalize(:nfc)
66
+ domain = Punycode.encode_hostname(domain)
67
+ end
68
+
69
+ domain.downcase
68
70
  end
69
71
  end
70
72
 
@@ -73,7 +75,7 @@ module HTTPX
73
75
  def initialize(hostname)
74
76
  hostname = String(hostname)
75
77
 
76
- raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(DOT)
78
+ raise ArgumentError, "domain name must not start with a dot: #{hostname}" if hostname.start_with?(".")
77
79
 
78
80
  begin
79
81
  @ipaddr = IPAddr.new(hostname)
@@ -84,7 +86,7 @@ module HTTPX
84
86
  end
85
87
 
86
88
  @hostname = DomainName.normalize(hostname)
87
- tld = if (last_dot = @hostname.rindex(DOT))
89
+ tld = if (last_dot = @hostname.rindex("."))
88
90
  @hostname[(last_dot + 1)..-1]
89
91
  else
90
92
  @hostname
@@ -94,7 +96,7 @@ module HTTPX
94
96
  @domain = if last_dot
95
97
  # fallback - accept cookies down to second level
96
98
  # cf. http://www.dkim-reputation.org/regdom-libs/
97
- if (penultimate_dot = @hostname.rindex(DOT, last_dot - 1))
99
+ if (penultimate_dot = @hostname.rindex(".", last_dot - 1))
98
100
  @hostname[(penultimate_dot + 1)..-1]
99
101
  else
100
102
  @hostname
@@ -126,17 +128,12 @@ module HTTPX
126
128
  @domain && self <= domain && domain <= @domain
127
129
  end
128
130
 
129
- # def ==(other)
130
- # other = DomainName.new(other)
131
- # other.hostname == @hostname
132
- # end
133
-
134
131
  def <=>(other)
135
132
  other = DomainName.new(other)
136
133
  othername = other.hostname
137
134
  if othername == @hostname
138
135
  0
139
- elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == DOT
136
+ elsif @hostname.end_with?(othername) && @hostname[-othername.size - 1, 1] == "."
140
137
  # The other is higher
141
138
  -1
142
139
  else