Class: HTTPX::Connection

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Callbacks, Loggable
Defined in:
lib/httpx/connection.rb

Overview

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.

Defined Under Namespace

Classes: HTTP1, HTTP2

Constant Summary

Constants included from Loggable

Loggable::COLORS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Callbacks

#callbacks_for?, #emit, #on, #once, #only

Methods included from Loggable

#log, #log_exception

Constructor Details

#initialize(type, uri, options) ⇒ Connection

Returns a new instance of Connection.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/httpx/connection.rb', line 51

def initialize(type, uri, options)
  @type = type
  @origins = [uri.origin]
  @origin = Utils.to_uri(uri.origin)
  @options = Options.new(options)
  @window_size = @options.window_size
  @read_buffer = Buffer.new(@options.buffer_size)
  @write_buffer = Buffer.new(@options.buffer_size)
  @pending = []
  on(:error, &method(:on_error))
  if @options.io
    # if there's an already open IO, get its
    # peer address, and force-initiate the parser
    transition(:already_open)
    @io = build_socket
    parser
  else
    transition(:idle)
  end

  @inflight = 0
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
  @total_timeout = @options.timeout[:total_timeout]

  self.addresses = @options.addresses if @options.addresses
end

Instance Attribute Details

#familyObject

Returns the value of attribute family.



49
50
51
# File 'lib/httpx/connection.rb', line 49

def family
  @family
end

#ioObject (readonly)

Returns the value of attribute io.



45
46
47
# File 'lib/httpx/connection.rb', line 45

def io
  @io
end

#optionsObject (readonly)

Returns the value of attribute options.



45
46
47
# File 'lib/httpx/connection.rb', line 45

def options
  @options
end

#originObject (readonly)

Returns the value of attribute origin.



45
46
47
# File 'lib/httpx/connection.rb', line 45

def origin
  @origin
end

#originsObject (readonly)

Returns the value of attribute origins.



45
46
47
# File 'lib/httpx/connection.rb', line 45

def origins
  @origins
end

#pendingObject (readonly)

Returns the value of attribute pending.



45
46
47
# File 'lib/httpx/connection.rb', line 45

def pending
  @pending
end

#stateObject (readonly)

Returns the value of attribute state.



45
46
47
# File 'lib/httpx/connection.rb', line 45

def state
  @state
end

#timers=(value) ⇒ Object (writeonly)

Sets the attribute timers

Parameters:

  • the value to set the attribute timers to.



47
48
49
# File 'lib/httpx/connection.rb', line 47

def timers=(value)
  @timers = value
end

#typeObject (readonly)

Returns the value of attribute type.



45
46
47
# File 'lib/httpx/connection.rb', line 45

def type
  @type
end

Class Method Details

.parser_type(protocol) ⇒ Object



677
678
679
680
681
682
683
684
# File 'lib/httpx/connection.rb', line 677

def parser_type(protocol)
  case protocol
  when "h2" then HTTP2
  when "http/1.1" then HTTP1
  else
    raise Error, "unsupported protocol (##{protocol})"
  end
end

Instance Method Details

#addressesObject



88
89
90
# File 'lib/httpx/connection.rb', line 88

def addresses
  @io && @io.addresses
end

#addresses=(addrs) ⇒ Object

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



80
81
82
83
84
85
86
# File 'lib/httpx/connection.rb', line 80

def addresses=(addrs)
  if @io
    @io.add_addresses(addrs)
  else
    @io = build_socket(addrs)
  end
end

#callObject



204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/httpx/connection.rb', line 204

def call
  case @state
  when :closed
    return
  when :closing
    consume
    transition(:closed)
    emit(:close)
  when :open
    consume
  end
  nil
end

#closeObject



218
219
220
221
222
# File 'lib/httpx/connection.rb', line 218

def close
  transition(:active) if @state == :inactive

  @parser.close if @parser
end

#coalescable?(connection) ⇒ Boolean

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

Returns:



124
125
126
127
128
129
130
131
132
133
# File 'lib/httpx/connection.rb', line 124

def coalescable?(connection)
  if @io.protocol == "h2" &&
     @origin.scheme == "https" &&
     connection.origin.scheme == "https" &&
     @io.can_verify_peer?
    @io.verify_hostname(connection.origin.host)
  else
    @origin == connection.origin
  end
end

#connecting?Boolean

Returns:



176
177
178
# File 'lib/httpx/connection.rb', line 176

def connecting?
  @state == :idle
end

#create_idle(options = {}) ⇒ Object



135
136
137
# File 'lib/httpx/connection.rb', line 135

def create_idle(options = {})
  self.class.new(@type, @origin, @options.merge(options))
end

#deactivateObject



283
284
285
# File 'lib/httpx/connection.rb', line 283

def deactivate
  transition(:inactive)
end

#force_resetObject

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



226
227
228
229
230
# File 'lib/httpx/connection.rb', line 226

def force_reset
  @state = :closing
  transition(:closed)
  emit(:close)
end

#inflight?Boolean

Returns:



180
181
182
# File 'lib/httpx/connection.rb', line 180

def inflight?
  @parser && !@parser.empty? && !@write_buffer.empty?
end

#interestsObject



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/httpx/connection.rb', line 184

def interests
  # connecting
  if connecting?
    connect

    return @io.interests if connecting?
  end

  # if the write buffer is full, we drain it
  return :w unless @write_buffer.empty?

  return @parser.interests if @parser

  nil
end

#match?(uri, options) ⇒ Boolean

Returns:



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/httpx/connection.rb', line 92

def match?(uri, options)
  return false if @state == :closing || @state == :closed

  return false if exhausted?

  (
    (
      @origins.include?(uri.origin) &&
      # if there is more than one origin to match, it means that this connection
      # was the result of coalescing. To prevent blind trust in the case where the
      # origin came from an ORIGIN frame, we're going to verify the hostname with the
      # SSL certificate
      (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
    ) && @options == options
  ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
end

#match_altsvc_options?(uri, options) ⇒ Boolean

Returns:



168
169
170
171
172
173
174
# File 'lib/httpx/connection.rb', line 168

def match_altsvc_options?(uri, options)
  return @options == options unless @options.ssl[:hostname] == uri.host

  dup_options = @options.merge(ssl: { hostname: nil })
  dup_options.ssl.delete(:hostname)
  dup_options == options
end

#match_altsvcs?(uri) ⇒ Boolean

checks if this is connection is an alternative service of uri

Returns:



160
161
162
163
164
165
166
# File 'lib/httpx/connection.rb', line 160

def match_altsvcs?(uri)
  @origins.any? { |origin| uri.altsvc_match?(origin) } ||
    AltSvc.cached_altsvc(@origin).any? do |altsvc|
      origin = altsvc["origin"]
      origin.altsvc_match?(uri.origin)
    end
end

#merge(connection) ⇒ Object



139
140
141
142
143
144
# File 'lib/httpx/connection.rb', line 139

def merge(connection)
  @origins |= connection.instance_variable_get(:@origins)
  connection.purge_pending do |req|
    send(req)
  end
end

#mergeable?(connection) ⇒ Boolean

Returns:



109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/httpx/connection.rb', line 109

def mergeable?(connection)
  return false if @state == :closing || @state == :closed || !@io

  return false if exhausted?

  return false unless connection.addresses

  (
    (open? && @origin == connection.origin) ||
    !(@io.addresses & (connection.addresses || [])).empty?
  ) && @options == connection.options
end

#open?Boolean

Returns:



287
288
289
# File 'lib/httpx/connection.rb', line 287

def open?
  @state == :open || @state == :inactive
end

#purge_pending(&block) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/httpx/connection.rb', line 146

def purge_pending(&block)
  pendings = []
  if @parser
    @inflight -= @parser.pending.size
    pendings << @parser.pending
  end
  pendings << @pending
  pendings.each do |pending|
    pending.reject!(&block)
  end
end

#raise_timeout_error(interval) ⇒ Object



291
292
293
294
295
# File 'lib/httpx/connection.rb', line 291

def raise_timeout_error(interval)
  error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
  error.set_backtrace(caller)
  on_error(error)
end

#resetObject



232
233
234
235
236
# File 'lib/httpx/connection.rb', line 232

def reset
  transition(:closing)
  transition(:closed)
  emit(:close)
end

#send(request) ⇒ Object



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/httpx/connection.rb', line 238

def send(request)
  if @parser && !@write_buffer.full?
    request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)

    if @response_received_at && @keep_alive_timeout &&
       Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
      # when pushing a request into an existing connection, we have to check whether there
      # is the possibility that the connection might have extended the keep alive timeout.
      # for such cases, we want to ping for availability before deciding to shovel requests.
      log(level: 3) { "keep alive timeout expired, pinging connection..." }
      @pending << request
      parser.ping
      transition(:active) if @state == :inactive
      return
    end

    send_request_to_parser(request)
  else
    @pending << request
  end
end

#timeoutObject



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/httpx/connection.rb', line 260

def timeout
  if @total_timeout
    return @total_timeout unless @connected_at

    elapsed_time = @total_timeout - Utils.elapsed_time(@connected_at)

    if elapsed_time.negative?
      ex = TotalTimeoutError.new(@total_timeout, "Timed out after #{@total_timeout} seconds")
      ex.set_backtrace(caller)
      on_error(ex)
      return
    end

    return elapsed_time
  end

  return @timeout if defined?(@timeout)

  return @options.timeout[:connect_timeout] if @state == :idle

  @options.timeout[:operation_timeout]
end

#to_ioObject



200
201
202
# File 'lib/httpx/connection.rb', line 200

def to_io
  @io.to_io
end