class HTTPX::Connection

  1. lib/httpx/connection.rb
  2. lib/httpx/connection/http1.rb
  3. lib/httpx/connection/http2.rb
  4. lib/httpx/plugins/h2c.rb
  5. show all
Superclass: Object

The Connection can be watched for IO events.

It contains the io object to read/write from, and knows what to do when it can.

It defers connecting until absolutely necessary. Connection should be triggered from the IO selector (until then, any request will be queued).

A connection boots up its parser after connection is established. All pending requests will be redirected there after connection.

A connection can be prevented from closing by the parser, that is, if there are pending requests. This will signal that the connection was prematurely closed, due to a possible number of conditions:

  • Remote peer closed the connection (“Connection: close”);

  • Remote peer doesn’t support pipelining;

A connection may also route requests for a different host for which the io was connected to, provided that the IP is the same and the port and scheme as well. This will allow to share the same socket to send HTTP/2 requests to different hosts.

Included modules

  1. Loggable
  2. Callbacks

Attributes

current_selector [W]
current_session [RW]
family [RW]
io [R]
options [R]
origin [R]
origins [R]
pending [R]
sibling [R]
ssl_session [R]
state [R]
type [R]

Public Class methods

new(uri, options)
[show source]
   # File lib/httpx/connection.rb
47 def initialize(uri, options)
48   @current_session = @current_selector = @max_concurrent_requests =
49                        @parser = @sibling = @coalesced_connection = @altsvc_connection =
50                                               @ping_timer = @family = @io = @ssl_session =
51                                                                         @timeout = @connected_at = @response_received_at = nil
52 
53   @exhausted = @cloned = @main_sibling = false
54 
55   @options = Options.new(options)
56   @type = initialize_type(uri, @options)
57   @origins = [uri.origin]
58   @origin = Utils.to_uri(uri.origin)
59   @window_size = @options.window_size
60   @read_buffer = Buffer.new(@options.buffer_size)
61   @write_buffer = Buffer.new(@options.buffer_size)
62   @pending = []
63   @inflight = 0
64   @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
65   @no_more_requests_counter = 0
66 
67   if @options.io
68     # if there's an already open IO, get its
69     # peer address, and force-initiate the parser
70     transition(:already_open)
71     @io = build_socket
72     parser
73   else
74     transition(:idle)
75   end
76   self.addresses = @options.addresses if @options.addresses
77 end

Public Instance methods

addresses()
[show source]
   # File lib/httpx/connection.rb
93 def addresses
94   @io && @io.addresses
95 end
addresses=(addrs)

this is a semi-private method, to be used by the resolver to initiate the io object.

[show source]
   # File lib/httpx/connection.rb
85 def addresses=(addrs)
86   if @io
87     @io.add_addresses(addrs)
88   else
89     @io = build_socket(addrs)
90   end
91 end
addresses?()
[show source]
   # File lib/httpx/connection.rb
97 def addresses?
98   @io && @io.addresses?
99 end
call()
[show source]
    # File lib/httpx/connection.rb
216 def call
217   case @state
218   when :idle
219     connect
220 
221     # when opening the tcp or ssl socket fails
222     return if @state == :closed
223 
224     consume
225   when :closed
226     return if @pending.empty?
227 
228     # there are pending requests to send, restart the state machine.
229     idling
230 
231     # @fiber-switch-guard
232     # fiber may have switch after ensuring that @io is closed.
233     return unless @state == :idle
234 
235     call
236   when :closing
237     consume
238     transition(:closed)
239 
240     # @fiber-switch-guard
241     # fiber may have switch while closing @io.
242     return if @state == :closed &&
243               # only remain here if there are pending requests.
244               @pending.empty?
245 
246     call
247   when :open
248     consume
249   end
250   nil
251 rescue IOError => e
252   @write_buffer.clear
253   on_io_error(e)
254 rescue StandardError => e
255   @write_buffer.clear
256   on_error(e)
257 rescue Exception => e # rubocop:disable Lint/RescueException
258   force_close(true)
259   raise e
260 end
close()
[show source]
    # File lib/httpx/connection.rb
262 def close
263   transition(:active) if @state == :inactive
264 
265   @parser.close if @parser
266 end
coalescable?(connection)

coalescable connections need to be mergeable! but internally, mergeable? is called before coalescable?

[show source]
    # File lib/httpx/connection.rb
138 def coalescable?(connection)
139   if @io.protocol == "h2" &&
140      @origin.scheme == "https" &&
141      connection.origin.scheme == "https" &&
142      @io.can_verify_peer?
143     @io.verify_hostname(connection.origin.host)
144   else
145     @origin == connection.origin
146   end
147 end
coalesce!(connection)

coalesces self into connection.

[show source]
    # File lib/httpx/connection.rb
125 def coalesce!(connection)
126   @coalesced_connection = connection
127 
128   close_sibling
129   connection.merge(self)
130 end
coalesced?()
[show source]
    # File lib/httpx/connection.rb
132 def coalesced?
133   @coalesced_connection
134 end
connecting?()
[show source]
    # File lib/httpx/connection.rb
183 def connecting?
184   @state == :idle
185 end
deactivate()
[show source]
    # File lib/httpx/connection.rb
387 def deactivate
388   transition(:inactive)
389 end
disconnect()

disconnects from the current session it’s attached to

[show source]
    # File lib/httpx/connection.rb
422 def disconnect
423   return if @exhausted # it'll reset
424 
425   return unless (current_session = @current_session) && (current_selector = @current_selector)
426 
427   @current_session = @current_selector = nil
428 
429   current_session.deselect_connection(self, current_selector, @cloned)
430 end
force_close(delete_pending = false)

bypasses state machine rules while setting the connection in the :closed state.

[show source]
    # File lib/httpx/connection.rb
287 def force_close(delete_pending = false)
288   force_purge
289   return unless @state == :closed
290 
291   if delete_pending
292     @pending.clear
293   elsif (parser = @parser)
294     enqueue_pending_requests_from_parser(parser)
295   end
296 
297   return unless @pending.empty?
298 
299   disconnect
300   emit(:force_closed, delete_pending)
301 end
force_reset(cloned = false)

bypasses the state machine to force closing of connections still connecting. only used for Happy Eyeballs v2.

[show source]
    # File lib/httpx/connection.rb
305 def force_reset(cloned = false)
306   @state = :closing
307   @cloned = cloned
308   transition(:closed)
309 end
handle_connect_error(error)
[show source]
    # File lib/httpx/connection.rb
413 def handle_connect_error(error)
414   return on_error(error) unless @sibling && @sibling.connecting?
415 
416   @sibling.merge(self)
417 
418   force_reset(true)
419 end
handle_socket_timeout(interval)
[show source]
    # File lib/httpx/connection.rb
395 def handle_socket_timeout(interval)
396   error = OperationTimeoutError.new(interval, "timed out while waiting on select")
397   error.set_backtrace(caller)
398   on_error(error)
399 end
idling()
[show source]
    # File lib/httpx/connection.rb
370 def idling
371   purge_after_closed
372 
373   return unless @state == :closed
374 
375   @write_buffer.clear
376   transition(:idle)
377   return unless @parser
378 
379   enqueue_pending_requests_from_parser(parser)
380   @parser = nil
381 end
inflight?()
[show source]
    # File lib/httpx/connection.rb
187 def inflight?
188   @parser && (
189     # parser may be dealing with other requests (possibly started from a different fiber)
190     !@parser.empty? ||
191     # connection may be doing connection termination handshake
192     !@write_buffer.empty?
193   )
194 end
inspect()

:nocov:

[show source]
    # File lib/httpx/connection.rb
466 def inspect
467   "#<#{self.class}:#{object_id} " \
468     "@origin=#{@origin} " \
469     "@state=#{@state} " \
470     "@pending=#{@pending.size} " \
471     "@io=#{@io}>"
472 end
interests()
[show source]
    # File lib/httpx/connection.rb
196 def interests
197   # connecting
198   if connecting?
199     connect
200 
201     return @io.interests if connecting?
202   end
203 
204   return @parser.interests if @parser
205 
206   nil
207 rescue StandardError => e
208   on_error(e)
209   nil
210 end
io_connected?()
[show source]
    # File lib/httpx/connection.rb
177 def io_connected?
178   return @coalesced_connection.io_connected? if @coalesced_connection
179 
180   @io && @io.state == :connected
181 end
match?(uri, options)
[show source]
    # File lib/httpx/connection.rb
101 def match?(uri, options)
102   return false if !used? && (@state == :closing || @state == :closed)
103 
104   @origins.include?(uri.origin) &&
105     # if there is more than one origin to match, it means that this connection
106     # was the result of coalescing. To prevent blind trust in the case where the
107     # origin came from an ORIGIN frame, we're going to verify the hostname with the
108     # SSL certificate
109     (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) &&
110     @options.connection_options_match?(options)
111 end
merge(connection)
[show source]
    # File lib/httpx/connection.rb
149 def merge(connection)
150   @origins |= connection.instance_variable_get(:@origins)
151   if @ssl_session.nil? && connection.ssl_session
152     @ssl_session = connection.ssl_session
153     @io.session_new_cb do |sess|
154       @ssl_session = sess
155     end if @io
156   end
157   connection.purge_pending do |req|
158     req.transition(:idle)
159     send(req)
160     true
161   end
162 end
mergeable?(connection)
[show source]
    # File lib/httpx/connection.rb
113 def mergeable?(connection)
114   return false if @state == :closing || @state == :closed || !@io
115 
116   return false unless connection.addresses
117 
118   (
119     (open? && @origin == connection.origin) ||
120     !(@io.addresses & (connection.addresses || [])).empty?
121   ) && @options.connection_options_match?(connection.options)
122 end
on_connect_error(e)
[show source]
    # File lib/httpx/connection.rb
432 def on_connect_error(e)
433   # connect errors, exit gracefully
434   error = ConnectionError.new(e.message)
435   error.set_backtrace(e.backtrace)
436   handle_connect_error(error) if connecting?
437   force_close
438 end
on_error(error, request = nil)
[show source]
    # File lib/httpx/connection.rb
445 def on_error(error, request = nil)
446   if error.is_a?(OperationTimeoutError)
447 
448     # inactive connections do not contribute to the select loop, therefore
449     # they should not fail due to such errors.
450     return if @state == :inactive
451 
452     if @timeout
453       @timeout -= error.timeout
454       return unless @timeout <= 0
455 
456       @timeout = nil
457     end
458 
459     error = error.to_connection_error if connecting?
460   end
461   handle_error(error, request)
462   reset
463 end
on_io_error(e)
[show source]
    # File lib/httpx/connection.rb
440 def on_io_error(e)
441   on_error(e)
442   force_close(true)
443 end
open?()
[show source]
    # File lib/httpx/connection.rb
391 def open?
392   @state == :open || @state == :inactive
393 end
peer()
[show source]
   # File lib/httpx/connection.rb
79 def peer
80   @origin
81 end
purge_pending(&block)
[show source]
    # File lib/httpx/connection.rb
164 def purge_pending(&block)
165   pendings = []
166   if @parser
167     pending = @parser.pending
168     @inflight -= pending.size
169     pendings << pending
170   end
171   pendings << @pending
172   pendings.each do |pending|
173     pending.reject!(&block)
174   end
175 end
reset()
[show source]
    # File lib/httpx/connection.rb
311 def reset
312   return if @state == :closing || @state == :closed
313 
314   # do not reset a connection which may have restarted back to :idle, such when the parser resets
315   # (example: HTTP/1 parser disabling pipelining)
316   return if @state == :idle && @pending.any?
317 
318   if @ping_timer
319     @ping_timer.cancel
320     @ping_timer = nil
321   end
322 
323   parser = @parser
324 
325   if parser && parser.respond_to?(:max_concurrent_requests)
326     # if connection being reset has at some downgraded the number of concurrent
327     # requests, such as in the case where an attempt to use HTTP/1 pipelining failed,
328     # keep that information around.
329     @max_concurrent_requests = parser.max_concurrent_requests
330   end
331 
332   transition(:closing)
333 
334   transition(:closed)
335 end
send(request)
[show source]
    # File lib/httpx/connection.rb
337 def send(request)
338   return @coalesced_connection.send(request) if @coalesced_connection
339 
340   if @parser && !@write_buffer.full?
341     if @response_received_at && @keep_alive_timeout &&
342        Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
343       # when pushing a request into an existing connection, we have to check whether there
344       # is the possibility that the connection might have extended the keep alive timeout.
345       # for such cases, we want to ping for availability before deciding to shovel requests.
346       log(level: 3) { "keep alive timeout expired, pinging connection..." }
347       @pending << request
348       transition(:active) if @state == :inactive
349       request.ping!
350       ping(request)
351       return
352     end
353 
354     send_request_to_parser(request)
355   else
356     @pending << request
357   end
358 end
sibling=(connection)
[show source]
    # File lib/httpx/connection.rb
401 def sibling=(connection)
402   @sibling = connection
403 
404   return unless connection
405 
406   @main_sibling = connection.sibling.nil?
407 
408   return unless @main_sibling
409 
410   connection.sibling = self
411 end
terminate()
[show source]
    # File lib/httpx/connection.rb
268 def terminate
269   case @state
270   when :idle
271     purge_after_closed
272 
273     # @fiber-switch-guard
274     if @io.can_disconnect? && @pending.empty?
275       disconnect
276       return
277     end
278   when :closed
279     @connected_at = nil
280   end
281 
282   close
283 end
timeout()
[show source]
    # File lib/httpx/connection.rb
360 def timeout
361   return if @state == :closed || @state == :inactive
362 
363   return @timeout if @timeout
364 
365   return @options.timeout[:connect_timeout] if @state == :idle
366 
367   @options.timeout[:operation_timeout]
368 end
to_io()
[show source]
    # File lib/httpx/connection.rb
212 def to_io
213   @io.to_io
214 end
used?()
[show source]
    # File lib/httpx/connection.rb
383 def used?
384   @connected_at
385 end