httpx 0.20.0 → 1.3.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 (250) 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 +5 -5
  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_19_8.md +1 -1
  15. data/doc/release_notes/0_20_0.md +2 -2
  16. data/doc/release_notes/0_20_1.md +5 -0
  17. data/doc/release_notes/0_20_2.md +7 -0
  18. data/doc/release_notes/0_20_3.md +6 -0
  19. data/doc/release_notes/0_20_4.md +17 -0
  20. data/doc/release_notes/0_20_5.md +3 -0
  21. data/doc/release_notes/0_21_0.md +96 -0
  22. data/doc/release_notes/0_21_1.md +12 -0
  23. data/doc/release_notes/0_22_0.md +13 -0
  24. data/doc/release_notes/0_22_1.md +11 -0
  25. data/doc/release_notes/0_22_2.md +5 -0
  26. data/doc/release_notes/0_22_3.md +55 -0
  27. data/doc/release_notes/0_22_4.md +6 -0
  28. data/doc/release_notes/0_22_5.md +6 -0
  29. data/doc/release_notes/0_23_0.md +42 -0
  30. data/doc/release_notes/0_23_1.md +5 -0
  31. data/doc/release_notes/0_23_2.md +5 -0
  32. data/doc/release_notes/0_23_3.md +6 -0
  33. data/doc/release_notes/0_23_4.md +5 -0
  34. data/doc/release_notes/0_24_0.md +48 -0
  35. data/doc/release_notes/0_24_1.md +12 -0
  36. data/doc/release_notes/0_24_2.md +12 -0
  37. data/doc/release_notes/0_24_3.md +12 -0
  38. data/doc/release_notes/0_24_4.md +18 -0
  39. data/doc/release_notes/0_24_5.md +6 -0
  40. data/doc/release_notes/0_24_6.md +5 -0
  41. data/doc/release_notes/0_24_7.md +10 -0
  42. data/doc/release_notes/1_0_0.md +60 -0
  43. data/doc/release_notes/1_0_1.md +5 -0
  44. data/doc/release_notes/1_0_2.md +7 -0
  45. data/doc/release_notes/1_1_0.md +32 -0
  46. data/doc/release_notes/1_1_1.md +17 -0
  47. data/doc/release_notes/1_1_2.md +12 -0
  48. data/doc/release_notes/1_1_3.md +18 -0
  49. data/doc/release_notes/1_1_4.md +6 -0
  50. data/doc/release_notes/1_1_5.md +12 -0
  51. data/doc/release_notes/1_2_0.md +49 -0
  52. data/doc/release_notes/1_2_1.md +6 -0
  53. data/doc/release_notes/1_2_2.md +10 -0
  54. data/doc/release_notes/1_2_3.md +16 -0
  55. data/doc/release_notes/1_2_4.md +8 -0
  56. data/doc/release_notes/1_2_5.md +7 -0
  57. data/doc/release_notes/1_2_6.md +13 -0
  58. data/doc/release_notes/1_3_0.md +18 -0
  59. data/doc/release_notes/1_3_1.md +17 -0
  60. data/lib/httpx/adapters/datadog.rb +215 -122
  61. data/lib/httpx/adapters/faraday.rb +145 -107
  62. data/lib/httpx/adapters/sentry.rb +26 -7
  63. data/lib/httpx/adapters/webmock.rb +34 -18
  64. data/lib/httpx/altsvc.rb +63 -26
  65. data/lib/httpx/base64.rb +27 -0
  66. data/lib/httpx/buffer.rb +12 -0
  67. data/lib/httpx/callbacks.rb +5 -3
  68. data/lib/httpx/chainable.rb +54 -39
  69. data/lib/httpx/connection/http1.rb +75 -44
  70. data/lib/httpx/connection/http2.rb +31 -38
  71. data/lib/httpx/connection.rb +287 -117
  72. data/lib/httpx/domain_name.rb +10 -13
  73. data/lib/httpx/errors.rb +52 -2
  74. data/lib/httpx/extensions.rb +24 -131
  75. data/lib/httpx/io/ssl.rb +83 -77
  76. data/lib/httpx/io/tcp.rb +48 -71
  77. data/lib/httpx/io/udp.rb +18 -52
  78. data/lib/httpx/io/unix.rb +10 -15
  79. data/lib/httpx/io.rb +3 -9
  80. data/lib/httpx/loggable.rb +4 -19
  81. data/lib/httpx/options.rb +176 -118
  82. data/lib/httpx/parser/http1.rb +4 -0
  83. data/lib/httpx/plugins/{authentication → auth}/basic.rb +1 -5
  84. data/lib/httpx/plugins/{authentication → auth}/digest.rb +14 -14
  85. data/lib/httpx/plugins/{authentication → auth}/ntlm.rb +1 -3
  86. data/lib/httpx/plugins/{authentication → auth}/socks5.rb +0 -2
  87. data/lib/httpx/plugins/auth.rb +25 -0
  88. data/lib/httpx/plugins/aws_sdk_authentication.rb +4 -3
  89. data/lib/httpx/plugins/aws_sigv4.rb +12 -9
  90. data/lib/httpx/plugins/basic_auth.rb +29 -0
  91. data/lib/httpx/plugins/brotli.rb +50 -0
  92. data/lib/httpx/plugins/callbacks.rb +91 -0
  93. data/lib/httpx/plugins/circuit_breaker/circuit.rb +100 -0
  94. data/lib/httpx/plugins/circuit_breaker/circuit_store.rb +53 -0
  95. data/lib/httpx/plugins/circuit_breaker.rb +148 -0
  96. data/lib/httpx/plugins/cookies/set_cookie_parser.rb +0 -2
  97. data/lib/httpx/plugins/cookies.rb +30 -17
  98. data/lib/httpx/plugins/{digest_authentication.rb → digest_auth.rb} +14 -12
  99. data/lib/httpx/plugins/expect.rb +21 -14
  100. data/lib/httpx/plugins/follow_redirects.rb +140 -41
  101. data/lib/httpx/plugins/grpc/call.rb +2 -3
  102. data/lib/httpx/plugins/grpc/grpc_encoding.rb +88 -0
  103. data/lib/httpx/plugins/grpc/message.rb +7 -37
  104. data/lib/httpx/plugins/grpc.rb +36 -29
  105. data/lib/httpx/plugins/h2c.rb +26 -19
  106. data/lib/httpx/plugins/internal_telemetry.rb +16 -0
  107. data/lib/httpx/plugins/{ntlm_authentication.rb → ntlm_auth.rb} +7 -5
  108. data/lib/httpx/plugins/oauth.rb +175 -0
  109. data/lib/httpx/plugins/persistent.rb +1 -1
  110. data/lib/httpx/plugins/proxy/http.rb +23 -13
  111. data/lib/httpx/plugins/proxy/socks4.rb +9 -7
  112. data/lib/httpx/plugins/proxy/socks5.rb +11 -9
  113. data/lib/httpx/plugins/proxy.rb +80 -61
  114. data/lib/httpx/plugins/push_promise.rb +1 -1
  115. data/lib/httpx/plugins/rate_limiter.rb +5 -1
  116. data/lib/httpx/plugins/response_cache/file_store.rb +40 -0
  117. data/lib/httpx/plugins/response_cache/store.rb +62 -25
  118. data/lib/httpx/plugins/response_cache.rb +105 -12
  119. data/lib/httpx/plugins/retries.rb +87 -17
  120. data/lib/httpx/plugins/ssrf_filter.rb +145 -0
  121. data/lib/httpx/plugins/stream.rb +27 -23
  122. data/lib/httpx/plugins/upgrade/h2.rb +4 -4
  123. data/lib/httpx/plugins/upgrade.rb +8 -10
  124. data/lib/httpx/plugins/webdav.rb +80 -0
  125. data/lib/httpx/pool/synch_pool.rb +93 -0
  126. data/lib/httpx/pool.rb +102 -27
  127. data/lib/httpx/punycode.rb +9 -291
  128. data/lib/httpx/request/body.rb +154 -0
  129. data/lib/httpx/request.rb +130 -146
  130. data/lib/httpx/resolver/https.rb +62 -27
  131. data/lib/httpx/resolver/multi.rb +9 -13
  132. data/lib/httpx/resolver/native.rb +192 -76
  133. data/lib/httpx/resolver/resolver.rb +34 -9
  134. data/lib/httpx/resolver/system.rb +16 -11
  135. data/lib/httpx/resolver.rb +38 -16
  136. data/lib/httpx/response/body.rb +242 -0
  137. data/lib/httpx/response/buffer.rb +96 -0
  138. data/lib/httpx/response.rb +159 -217
  139. data/lib/httpx/selector.rb +9 -4
  140. data/lib/httpx/session.rb +137 -89
  141. data/lib/httpx/session_extensions.rb +4 -1
  142. data/lib/httpx/timers.rb +34 -8
  143. data/lib/httpx/transcoder/body.rb +0 -2
  144. data/lib/httpx/transcoder/chunker.rb +0 -1
  145. data/lib/httpx/transcoder/deflate.rb +37 -0
  146. data/lib/httpx/transcoder/form.rb +52 -33
  147. data/lib/httpx/transcoder/gzip.rb +74 -0
  148. data/lib/httpx/transcoder/json.rb +21 -8
  149. data/lib/httpx/transcoder/multipart/decoder.rb +139 -0
  150. data/lib/httpx/{plugins → transcoder}/multipart/encoder.rb +4 -4
  151. data/lib/httpx/{plugins → transcoder}/multipart/mime_type_detector.rb +1 -1
  152. data/lib/httpx/{plugins → transcoder}/multipart/part.rb +3 -2
  153. data/lib/httpx/transcoder/multipart.rb +17 -0
  154. data/lib/httpx/transcoder/utils/body_reader.rb +46 -0
  155. data/lib/httpx/transcoder/utils/deflater.rb +72 -0
  156. data/lib/httpx/transcoder/utils/inflater.rb +19 -0
  157. data/lib/httpx/transcoder/xml.rb +52 -0
  158. data/lib/httpx/transcoder.rb +5 -6
  159. data/lib/httpx/utils.rb +36 -16
  160. data/lib/httpx/version.rb +1 -1
  161. data/lib/httpx.rb +12 -14
  162. data/sig/altsvc.rbs +33 -0
  163. data/sig/buffer.rbs +2 -1
  164. data/sig/callbacks.rbs +3 -3
  165. data/sig/chainable.rbs +11 -9
  166. data/sig/connection/http1.rbs +8 -7
  167. data/sig/connection/http2.rbs +19 -19
  168. data/sig/connection.rbs +64 -24
  169. data/sig/errors.rbs +22 -3
  170. data/sig/httpx.rbs +5 -4
  171. data/sig/io/ssl.rbs +27 -0
  172. data/sig/io/tcp.rbs +60 -0
  173. data/sig/io/udp.rbs +20 -0
  174. data/sig/io/unix.rbs +27 -0
  175. data/sig/io.rbs +6 -0
  176. data/sig/options.rbs +32 -22
  177. data/sig/parser/http1.rbs +1 -1
  178. data/sig/plugins/{authentication → auth}/basic.rbs +0 -2
  179. data/sig/plugins/{authentication → auth}/digest.rbs +2 -1
  180. data/sig/plugins/auth.rbs +13 -0
  181. data/sig/plugins/{basic_authentication.rbs → basic_auth.rbs} +2 -2
  182. data/sig/plugins/brotli.rbs +22 -0
  183. data/sig/plugins/callbacks.rbs +38 -0
  184. data/sig/plugins/circuit_breaker.rbs +71 -0
  185. data/sig/plugins/compression.rbs +7 -5
  186. data/sig/plugins/cookies/jar.rbs +2 -2
  187. data/sig/plugins/cookies.rbs +2 -0
  188. data/sig/plugins/{digest_authentication.rbs → digest_auth.rbs} +2 -2
  189. data/sig/plugins/follow_redirects.rbs +18 -4
  190. data/sig/plugins/grpc/call.rbs +19 -0
  191. data/sig/plugins/grpc/grpc_encoding.rbs +37 -0
  192. data/sig/plugins/grpc/message.rbs +17 -0
  193. data/sig/plugins/grpc.rbs +7 -32
  194. data/sig/plugins/h2c.rbs +1 -1
  195. data/sig/plugins/{ntlm_authentication.rbs → ntlm_auth.rbs} +2 -2
  196. data/sig/plugins/oauth.rbs +54 -0
  197. data/sig/plugins/proxy/http.rbs +3 -0
  198. data/sig/plugins/proxy/socks4.rbs +9 -6
  199. data/sig/plugins/proxy/socks5.rbs +10 -6
  200. data/sig/plugins/proxy/ssh.rbs +1 -1
  201. data/sig/plugins/proxy.rbs +13 -5
  202. data/sig/plugins/push_promise.rbs +3 -3
  203. data/sig/plugins/rate_limiter.rbs +1 -1
  204. data/sig/plugins/response_cache.rbs +36 -7
  205. data/sig/plugins/retries.rbs +30 -8
  206. data/sig/plugins/stream.rbs +24 -17
  207. data/sig/plugins/upgrade.rbs +5 -3
  208. data/sig/pool.rbs +10 -7
  209. data/sig/request/body.rbs +38 -0
  210. data/sig/request.rbs +15 -24
  211. data/sig/resolver/https.rbs +8 -3
  212. data/sig/resolver/native.rbs +17 -4
  213. data/sig/resolver/resolver.rbs +8 -6
  214. data/sig/resolver/system.rbs +2 -0
  215. data/sig/resolver.rbs +9 -5
  216. data/sig/response/body.rbs +53 -0
  217. data/sig/response/buffer.rbs +24 -0
  218. data/sig/response.rbs +24 -39
  219. data/sig/selector.rbs +1 -1
  220. data/sig/session.rbs +29 -18
  221. data/sig/timers.rbs +18 -8
  222. data/sig/transcoder/body.rbs +4 -3
  223. data/sig/transcoder/deflate.rbs +11 -0
  224. data/sig/transcoder/form.rbs +5 -3
  225. data/sig/transcoder/gzip.rbs +24 -0
  226. data/sig/transcoder/json.rbs +8 -3
  227. data/sig/{plugins → transcoder}/multipart.rbs +15 -19
  228. data/sig/transcoder/utils/body_reader.rbs +15 -0
  229. data/sig/transcoder/utils/deflater.rbs +29 -0
  230. data/sig/transcoder/utils/inflater.rbs +12 -0
  231. data/sig/transcoder/xml.rbs +22 -0
  232. data/sig/transcoder.rbs +24 -9
  233. data/sig/utils.rbs +8 -2
  234. metadata +163 -41
  235. data/lib/httpx/plugins/authentication.rb +0 -20
  236. data/lib/httpx/plugins/basic_authentication.rb +0 -30
  237. data/lib/httpx/plugins/compression/brotli.rb +0 -54
  238. data/lib/httpx/plugins/compression/deflate.rb +0 -49
  239. data/lib/httpx/plugins/compression/gzip.rb +0 -88
  240. data/lib/httpx/plugins/compression.rb +0 -164
  241. data/lib/httpx/plugins/multipart/decoder.rb +0 -187
  242. data/lib/httpx/plugins/multipart.rb +0 -84
  243. data/lib/httpx/registry.rb +0 -85
  244. data/sig/plugins/authentication.rbs +0 -11
  245. data/sig/plugins/compression/brotli.rbs +0 -21
  246. data/sig/plugins/compression/deflate.rbs +0 -17
  247. data/sig/plugins/compression/gzip.rbs +0 -29
  248. data/sig/registry.rbs +0 -12
  249. /data/sig/plugins/{authentication → auth}/ntlm.rbs +0 -0
  250. /data/sig/plugins/{authentication → auth}/socks5.rbs +0 -0
@@ -29,7 +29,6 @@ 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
 
@@ -38,31 +37,33 @@ module HTTPX
38
37
  require "httpx/connection/http2"
39
38
  require "httpx/connection/http1"
40
39
 
41
- BUFFER_SIZE = 1 << 14
42
-
43
40
  def_delegator :@io, :closed?
44
41
 
45
42
  def_delegator :@write_buffer, :empty?
46
43
 
47
- attr_reader :io, :origin, :origins, :state, :pending, :options
44
+ attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session
48
45
 
49
46
  attr_writer :timers
50
47
 
51
- def initialize(type, uri, options)
52
- @type = type
48
+ attr_accessor :family
49
+
50
+ def initialize(uri, options)
53
51
  @origins = [uri.origin]
54
52
  @origin = Utils.to_uri(uri.origin)
55
53
  @options = Options.new(options)
54
+ @type = initialize_type(uri, @options)
55
+ @origins = [uri.origin]
56
+ @origin = Utils.to_uri(uri.origin)
56
57
  @window_size = @options.window_size
57
- @read_buffer = Buffer.new(BUFFER_SIZE)
58
- @write_buffer = Buffer.new(BUFFER_SIZE)
58
+ @read_buffer = Buffer.new(@options.buffer_size)
59
+ @write_buffer = Buffer.new(@options.buffer_size)
59
60
  @pending = []
60
61
  on(:error, &method(:on_error))
61
62
  if @options.io
62
63
  # if there's an already open IO, get its
63
64
  # peer address, and force-initiate the parser
64
65
  transition(:already_open)
65
- @io = IO.registry(@type).new(@origin, nil, @options)
66
+ @io = build_socket
66
67
  parser
67
68
  else
68
69
  transition(:idle)
@@ -70,7 +71,8 @@ module HTTPX
70
71
 
71
72
  @inflight = 0
72
73
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
73
- @total_timeout = @options.timeout[:total_timeout]
74
+
75
+ @intervals = []
74
76
 
75
77
  self.addresses = @options.addresses if @options.addresses
76
78
  end
@@ -81,7 +83,7 @@ module HTTPX
81
83
  if @io
82
84
  @io.add_addresses(addrs)
83
85
  else
84
- @io = IO.registry(@type).new(@origin, addrs, @options)
86
+ @io = build_socket(addrs)
85
87
  end
86
88
  end
87
89
 
@@ -90,30 +92,33 @@ module HTTPX
90
92
  end
91
93
 
92
94
  def match?(uri, options)
93
- return false if @state == :closing || @state == :closed
94
-
95
- return false if exhausted?
95
+ return false if !used? && (@state == :closing || @state == :closed)
96
96
 
97
97
  (
98
- (
99
- @origins.include?(uri.origin) &&
100
- # if there is more than one origin to match, it means that this connection
101
- # was the result of coalescing. To prevent blind trust in the case where the
102
- # origin came from an ORIGIN frame, we're going to verify the hostname with the
103
- # SSL certificate
104
- (@origins.size == 1 || @origin == uri.origin || (@io && @io.verify_hostname(uri.host)))
105
- ) && @options == options
106
- ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
98
+ @origins.include?(uri.origin) &&
99
+ # if there is more than one origin to match, it means that this connection
100
+ # was the result of coalescing. To prevent blind trust in the case where the
101
+ # origin came from an ORIGIN frame, we're going to verify the hostname with the
102
+ # SSL certificate
103
+ (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
104
+ ) && @options == options
105
+ end
106
+
107
+ def expired?
108
+ return false unless @io
109
+
110
+ @io.expired?
107
111
  end
108
112
 
109
113
  def mergeable?(connection)
110
114
  return false if @state == :closing || @state == :closed || !@io
111
115
 
112
- return false if exhausted?
113
-
114
116
  return false unless connection.addresses
115
117
 
116
- !(@io.addresses & connection.addresses).empty? && @options == connection.options
118
+ (
119
+ (open? && @origin == connection.origin) ||
120
+ !(@io.addresses & (connection.addresses || [])).empty?
121
+ ) && @options == connection.options
117
122
  end
118
123
 
119
124
  # coalescable connections need to be mergeable!
@@ -130,11 +135,17 @@ module HTTPX
130
135
  end
131
136
 
132
137
  def create_idle(options = {})
133
- self.class.new(@type, @origin, @options.merge(options))
138
+ self.class.new(@origin, @options.merge(options))
134
139
  end
135
140
 
136
141
  def merge(connection)
137
142
  @origins |= connection.instance_variable_get(:@origins)
143
+ if connection.ssl_session
144
+ @ssl_session = connection.ssl_session
145
+ @io.session_new_cb do |sess|
146
+ @ssl_session = sess
147
+ end if @io
148
+ end
138
149
  connection.purge_pending do |req|
139
150
  send(req)
140
151
  end
@@ -152,24 +163,6 @@ module HTTPX
152
163
  end
153
164
  end
154
165
 
155
- # checks if this is connection is an alternative service of
156
- # +uri+
157
- def match_altsvcs?(uri)
158
- @origins.any? { |origin| uri.altsvc_match?(origin) } ||
159
- AltSvc.cached_altsvc(@origin).any? do |altsvc|
160
- origin = altsvc["origin"]
161
- origin.altsvc_match?(uri.origin)
162
- end
163
- end
164
-
165
- def match_altsvc_options?(uri, options)
166
- return @options == options unless @options.ssl[:hostname] == uri.host
167
-
168
- dup_options = @options.merge(ssl: { hostname: nil })
169
- dup_options.ssl.delete(:hostname)
170
- dup_options == options
171
- end
172
-
173
166
  def connecting?
174
167
  @state == :idle
175
168
  end
@@ -200,12 +193,14 @@ module HTTPX
200
193
 
201
194
  def call
202
195
  case @state
196
+ when :idle
197
+ connect
198
+ consume
203
199
  when :closed
204
200
  return
205
201
  when :closing
206
202
  consume
207
203
  transition(:closed)
208
- emit(:close)
209
204
  when :open
210
205
  consume
211
206
  end
@@ -218,24 +213,38 @@ module HTTPX
218
213
  @parser.close if @parser
219
214
  end
220
215
 
216
+ def terminate
217
+ @connected_at = nil if @state == :closed
218
+
219
+ close
220
+ end
221
+
222
+ # bypasses the state machine to force closing of connections still connecting.
223
+ # **only** used for Happy Eyeballs v2.
224
+ def force_reset
225
+ @state = :closing
226
+ transition(:closed)
227
+ end
228
+
221
229
  def reset
230
+ return if @state == :closing || @state == :closed
231
+
222
232
  transition(:closing)
233
+
223
234
  transition(:closed)
224
- emit(:close)
225
235
  end
226
236
 
227
237
  def send(request)
228
238
  if @parser && !@write_buffer.full?
229
- request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
230
-
231
239
  if @response_received_at && @keep_alive_timeout &&
232
240
  Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
233
241
  # when pushing a request into an existing connection, we have to check whether there
234
242
  # is the possibility that the connection might have extended the keep alive timeout.
235
243
  # for such cases, we want to ping for availability before deciding to shovel requests.
244
+ log(level: 3) { "keep alive timeout expired, pinging connection..." }
236
245
  @pending << request
237
- parser.ping
238
246
  transition(:active) if @state == :inactive
247
+ parser.ping
239
248
  return
240
249
  end
241
250
 
@@ -246,28 +255,24 @@ module HTTPX
246
255
  end
247
256
 
248
257
  def timeout
249
- if @total_timeout
250
- return @total_timeout unless @connected_at
251
-
252
- elapsed_time = @total_timeout - Utils.elapsed_time(@connected_at)
253
-
254
- if elapsed_time.negative?
255
- ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
256
- ex.set_backtrace(caller)
257
- on_error(ex)
258
- return
259
- end
260
-
261
- return elapsed_time
262
- end
263
-
264
- return @timeout if defined?(@timeout)
258
+ return @timeout if @timeout
265
259
 
266
260
  return @options.timeout[:connect_timeout] if @state == :idle
267
261
 
268
262
  @options.timeout[:operation_timeout]
269
263
  end
270
264
 
265
+ def idling
266
+ purge_after_closed
267
+ @write_buffer.clear
268
+ transition(:idle)
269
+ @parser = nil if @parser
270
+ end
271
+
272
+ def used?
273
+ @connected_at
274
+ end
275
+
271
276
  def deactivate
272
277
  transition(:inactive)
273
278
  end
@@ -276,22 +281,35 @@ module HTTPX
276
281
  @state == :open || @state == :inactive
277
282
  end
278
283
 
284
+ def handle_socket_timeout(interval)
285
+ @intervals.delete_if(&:elapsed?)
286
+
287
+ unless @intervals.empty?
288
+ # remove the intervals which will elapse
289
+
290
+ return
291
+ end
292
+
293
+ error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
294
+ error.set_backtrace(caller)
295
+ on_error(error)
296
+ end
297
+
279
298
  private
280
299
 
281
300
  def connect
282
301
  transition(:open)
283
302
  end
284
303
 
285
- def exhausted?
286
- @parser && parser.exhausted?
287
- end
288
-
289
304
  def consume
290
305
  return unless @io
291
306
 
292
307
  catch(:called) do
293
308
  epiped = false
294
309
  loop do
310
+ # connection may have
311
+ return if @state == :idle
312
+
295
313
  parser.consume
296
314
 
297
315
  # we exit if there's no more requests to process
@@ -301,7 +319,7 @@ module HTTPX
301
319
  # * the number of inflight requests
302
320
  # * the number of pending requests
303
321
  # * whether the write buffer has bytes (i.e. for close handshake)
304
- if @pending.size.zero? && @inflight.zero? && @write_buffer.empty?
322
+ if @pending.empty? && @inflight.zero? && @write_buffer.empty?
305
323
  log(level: 3) { "NO MORE REQUESTS..." }
306
324
  return
307
325
  end
@@ -321,7 +339,7 @@ module HTTPX
321
339
  #
322
340
  loop do
323
341
  siz = @io.read(@window_size, @read_buffer)
324
- log(level: 3, color: :cyan) { "IO READ: #{siz} bytes..." }
342
+ log(level: 3, color: :cyan) { "IO READ: #{siz} bytes... (wsize: #{@window_size}, rbuffer: #{@read_buffer.bytesize})" }
325
343
  unless siz
326
344
  ex = EOFError.new("descriptor closed")
327
345
  ex.set_backtrace(caller)
@@ -345,7 +363,7 @@ module HTTPX
345
363
  break if @state == :closing || @state == :closed
346
364
 
347
365
  # exit #consume altogether if all outstanding requests have been dealt with
348
- return if @pending.size.zero? && @inflight.zero?
366
+ return if @pending.empty? && @inflight.zero?
349
367
  end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
350
368
 
351
369
  #
@@ -422,15 +440,18 @@ module HTTPX
422
440
 
423
441
  def send_request_to_parser(request)
424
442
  @inflight += 1
443
+ request.peer_address = @io.ip
425
444
  parser.send(request)
426
445
 
446
+ set_request_timeouts(request)
447
+
427
448
  return unless @state == :inactive
428
449
 
429
450
  transition(:active)
430
451
  end
431
452
 
432
453
  def build_parser(protocol = @io.protocol)
433
- parser = registry(protocol).new(@write_buffer, @options)
454
+ parser = self.class.parser_type(protocol).new(@write_buffer, @options)
434
455
  set_parser_callbacks(parser)
435
456
  parser
436
457
  end
@@ -454,33 +475,25 @@ module HTTPX
454
475
  request.emit(:promise, parser, stream)
455
476
  end
456
477
  parser.on(:exhausted) do
478
+ @pending.concat(parser.pending)
457
479
  emit(:exhausted)
458
480
  end
459
481
  parser.on(:origin) do |origin|
460
482
  @origins |= [origin]
461
483
  end
462
484
  parser.on(:close) do |force|
463
- transition(:closing)
464
- if force || @state == :idle
465
- transition(:closed)
466
- emit(:close)
485
+ if force
486
+ reset
487
+ emit(:terminate)
467
488
  end
468
489
  end
469
490
  parser.on(:close_handshake) do
470
491
  consume
471
492
  end
472
493
  parser.on(:reset) do
473
- if parser.empty?
474
- reset
475
- else
476
- transition(:closing)
477
- transition(:closed)
478
- emit(:reset)
479
-
480
- @parser.reset if @parser
481
- transition(:idle)
482
- transition(:open)
483
- end
494
+ @pending.concat(parser.pending) unless parser.empty?
495
+ reset
496
+ idling unless @pending.empty?
484
497
  end
485
498
  parser.on(:current_timeout) do
486
499
  @current_timeout = @timeout = parser.timeout
@@ -493,7 +506,7 @@ module HTTPX
493
506
  when MisdirectedRequestError
494
507
  emit(:misdirected, request)
495
508
  else
496
- response = ErrorResponse.new(request, ex, @options)
509
+ response = ErrorResponse.new(request, ex)
497
510
  request.response = response
498
511
  request.emit(:response, response)
499
512
  end
@@ -502,12 +515,27 @@ module HTTPX
502
515
 
503
516
  def transition(nextstate)
504
517
  handle_transition(nextstate)
505
- rescue Errno::ECONNREFUSED,
518
+ rescue Errno::ECONNABORTED,
519
+ Errno::ECONNREFUSED,
520
+ Errno::ECONNRESET,
506
521
  Errno::EADDRNOTAVAIL,
507
522
  Errno::EHOSTUNREACH,
508
- TLSError => e
523
+ Errno::EINVAL,
524
+ Errno::ENETUNREACH,
525
+ Errno::EPIPE,
526
+ Errno::ENOENT,
527
+ SocketError,
528
+ IOError => e
529
+ # connect errors, exit gracefully
530
+ error = ConnectionError.new(e.message)
531
+ error.set_backtrace(e.backtrace)
532
+ connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, error) : handle_error(error)
533
+ @state = :closed
534
+ emit(:close)
535
+ rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
509
536
  # connect errors, exit gracefully
510
537
  handle_error(e)
538
+ connecting? && callbacks_for?(:connect_error) ? emit(:connect_error, e) : handle_error(e)
511
539
  @state = :closed
512
540
  emit(:close)
513
541
  end
@@ -517,10 +545,13 @@ module HTTPX
517
545
  when :idle
518
546
  @timeout = @current_timeout = @options.timeout[:connect_timeout]
519
547
 
548
+ @connected_at = nil
520
549
  when :open
521
550
  return if @state == :closed
522
551
 
523
552
  @io.connect
553
+ emit(:tcp_open, self) if @io.state == :connected
554
+
524
555
  return unless @io.connected?
525
556
 
526
557
  @connected_at = Utils.now
@@ -531,16 +562,32 @@ module HTTPX
531
562
  emit(:open)
532
563
  when :inactive
533
564
  return unless @state == :open
534
- when :closing
535
- return unless @state == :open
536
565
 
566
+ # do not deactivate connection in use
567
+ return if @inflight.positive?
568
+ when :closing
569
+ return unless @state == :idle || @state == :open
570
+
571
+ unless @write_buffer.empty?
572
+ # preset state before handshake, as error callbacks
573
+ # may take it back here.
574
+ @state = nextstate
575
+ # handshakes, try sending
576
+ consume
577
+ @write_buffer.clear
578
+ return
579
+ end
537
580
  when :closed
538
581
  return unless @state == :closing
539
582
  return unless @write_buffer.empty?
540
583
 
541
584
  purge_after_closed
585
+ emit(:close) if @pending.empty?
542
586
  when :already_open
543
587
  nextstate = :open
588
+ # the first check for given io readiness must still use a timeout.
589
+ # connect is the reasonable choice in such a case.
590
+ @timeout = @options.timeout[:connect_timeout]
544
591
  send_pending
545
592
  when :active
546
593
  return unless @state == :inactive
@@ -554,40 +601,163 @@ module HTTPX
554
601
  def purge_after_closed
555
602
  @io.close if @io
556
603
  @read_buffer.clear
557
- remove_instance_variable(:@timeout) if defined?(@timeout)
604
+ @timeout = nil
558
605
  end
559
606
 
560
- def on_error(error)
561
- if error.instance_of?(TimeoutError)
562
-
563
- if @total_timeout && @connected_at &&
564
- Utils.elapsed_time(@connected_at) > @total_timeout
565
- ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
566
- ex.set_backtrace(error.backtrace)
567
- error = ex
607
+ def initialize_type(uri, options)
608
+ options.transport || begin
609
+ case uri.scheme
610
+ when "http"
611
+ "tcp"
612
+ when "https"
613
+ "ssl"
568
614
  else
569
- # inactive connections do not contribute to the select loop, therefore
570
- # they should fail due to such errors.
571
- return if @state == :inactive
615
+ raise UnsupportedSchemeError, "#{uri}: #{uri.scheme}: unsupported URI scheme"
616
+ end
617
+ end
618
+ end
572
619
 
573
- if @timeout
574
- @timeout -= error.timeout
575
- return unless @timeout <= 0
620
+ def build_socket(addrs = nil)
621
+ case @type
622
+ when "tcp"
623
+ TCP.new(@origin, addrs, @options)
624
+ when "ssl"
625
+ SSL.new(@origin, addrs, @options) do |sock|
626
+ sock.ssl_session = @ssl_session
627
+ sock.session_new_cb do |sess|
628
+ @ssl_session = sess
629
+
630
+ sock.ssl_session = sess
576
631
  end
632
+ end
633
+ when "unix"
634
+ path = Array(addrs).first
635
+
636
+ path = String(path) if path
637
+
638
+ UNIX.new(@origin, path, @options)
639
+ else
640
+ raise Error, "unsupported transport (#{@type})"
641
+ end
642
+ end
643
+
644
+ def on_error(error, request = nil)
645
+ if error.instance_of?(TimeoutError)
646
+
647
+ # inactive connections do not contribute to the select loop, therefore
648
+ # they should not fail due to such errors.
649
+ return if @state == :inactive
577
650
 
578
- error = error.to_connection_error if connecting?
651
+ if @timeout
652
+ @timeout -= error.timeout
653
+ return unless @timeout <= 0
579
654
  end
655
+
656
+ error = error.to_connection_error if connecting?
580
657
  end
581
- handle_error(error)
658
+ handle_error(error, request)
582
659
  reset
583
660
  end
584
661
 
585
- def handle_error(error)
586
- parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
587
- while (request = @pending.shift)
588
- response = ErrorResponse.new(request, error, @options)
589
- request.response = response
590
- request.emit(:response, response)
662
+ def handle_error(error, request = nil)
663
+ parser.handle_error(error, request) if @parser && parser.respond_to?(:handle_error)
664
+ while (req = @pending.shift)
665
+ next if request && req == request
666
+
667
+ response = ErrorResponse.new(req, error)
668
+ req.response = response
669
+ req.emit(:response, response)
670
+ end
671
+
672
+ return unless request
673
+
674
+ response = ErrorResponse.new(request, error)
675
+ request.response = response
676
+ request.emit(:response, response)
677
+ end
678
+
679
+ def set_request_timeouts(request)
680
+ set_request_write_timeout(request)
681
+ set_request_read_timeout(request)
682
+ set_request_request_timeout(request)
683
+ end
684
+
685
+ def set_request_read_timeout(request)
686
+ read_timeout = request.read_timeout
687
+
688
+ return if read_timeout.nil? || read_timeout.infinite?
689
+
690
+ set_request_timeout(request, read_timeout, :done, :response) do
691
+ read_timeout_callback(request, read_timeout)
692
+ end
693
+ end
694
+
695
+ def set_request_write_timeout(request)
696
+ write_timeout = request.write_timeout
697
+
698
+ return if write_timeout.nil? || write_timeout.infinite?
699
+
700
+ set_request_timeout(request, write_timeout, :headers, %i[done response]) do
701
+ write_timeout_callback(request, write_timeout)
702
+ end
703
+ end
704
+
705
+ def set_request_request_timeout(request)
706
+ request_timeout = request.request_timeout
707
+
708
+ return if request_timeout.nil? || request_timeout.infinite?
709
+
710
+ set_request_timeout(request, request_timeout, :headers, :complete) do
711
+ read_timeout_callback(request, request_timeout, RequestTimeoutError)
712
+ end
713
+ end
714
+
715
+ def write_timeout_callback(request, write_timeout)
716
+ return if request.state == :done
717
+
718
+ @write_buffer.clear
719
+ error = WriteTimeoutError.new(request, nil, write_timeout)
720
+
721
+ on_error(error, request)
722
+ end
723
+
724
+ def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
725
+ response = request.response
726
+
727
+ return if response && response.finished?
728
+
729
+ @write_buffer.clear
730
+ error = error_type.new(request, request.response, read_timeout)
731
+
732
+ on_error(error, request)
733
+ end
734
+
735
+ def set_request_timeout(request, timeout, start_event, finish_events, &callback)
736
+ request.once(start_event) do
737
+ interval = @timers.after(timeout, callback)
738
+
739
+ Array(finish_events).each do |event|
740
+ # clean up request timeouts if the connection errors out
741
+ request.once(event) do
742
+ if @intervals.include?(interval)
743
+ interval.delete(callback)
744
+ @intervals.delete(interval) if interval.no_callbacks?
745
+ end
746
+ end
747
+ end
748
+
749
+ @intervals << interval
750
+ end
751
+ end
752
+
753
+ class << self
754
+ def parser_type(protocol)
755
+ case protocol
756
+ when "h2" then HTTP2
757
+ when "http/1.1" then HTTP1
758
+ else
759
+ raise Error, "unsupported protocol (##{protocol})"
760
+ end
591
761
  end
592
762
  end
593
763
  end