Class: HTTPX::Connection::HTTP1
- Inherits:
-
Object
- Object
- HTTPX::Connection::HTTP1
- Includes:
- HTTPX::Callbacks, Loggable
- Defined in:
- lib/httpx/connection/http1.rb,
sig/connection/http1.rbs
Constant Summary collapse
- MAX_REQUESTS =
200- CRLF =
"\r\n"- UPCASED =
{ "www-authenticate" => "WWW-Authenticate", "http2-settings" => "HTTP2-Settings", "content-md5" => "Content-MD5", "last-event-id" => "Last-Event-ID", }.freeze
Constants included from Loggable
Loggable::COLORS, Loggable::USE_DEBUG_LOG
Instance Attribute Summary collapse
-
#max_concurrent_requests ⇒ Integer
Returns the value of attribute max_concurrent_requests.
-
#pending ⇒ Array[Request]
readonly
Returns the value of attribute pending.
-
#requests ⇒ Array[Request]
readonly
Returns the value of attribute requests.
Instance Method Summary collapse
- #<<(data) ⇒ void
- #capitalized(field) ⇒ String
- #close ⇒ void
- #consume ⇒ void
- #disable ⇒ void
- #disable_pipelining ⇒ void
- #dispatch(request) ⇒ void
- #empty? ⇒ Boolean
- #exhausted? ⇒ Boolean
- #handle(request) ⇒ void
- #handle_error(ex, request = nil) ⇒ void
-
#initialize(buffer, options) ⇒ HTTP1
constructor
A new instance of HTTP1.
- #interests ⇒ io_interests?
- #join_body(request) ⇒ void
- #join_headers(request) ⇒ void
- #join_headers2(request, headers) ⇒ void
- #join_headline(request) ⇒ String
- #join_trailers(request) ⇒ void
- #manage_connection(request, response) ⇒ void
- #on_complete ⇒ void
- #on_data(chunk) ⇒ void
- #on_headers(h) ⇒ void
-
#on_start ⇒ void
HTTP Parser callbacks.
- #on_trailers(h) ⇒ void
- #ping ⇒ void
- #reset ⇒ void
- #reset_requests ⇒ void
- #send(request) ⇒ void
- #set_protocol_headers(request) ⇒ _Each[[String, String]]
- #timeout ⇒ interval?
- #waiting_for_ping? ⇒ Boolean
Methods included from Loggable
#log, #log_exception, log_identifiers, #log_redact, #log_redact_body, #log_redact_headers
Methods included from HTTPX::Callbacks
#callbacks, #callbacks_for?, #emit, #on, #once
Constructor Details
#initialize(buffer, options) ⇒ HTTP1
Returns a new instance of HTTP1.
23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/httpx/connection/http1.rb', line 23 def initialize(buffer, ) @options = @max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS @max_requests = @options.max_requests @parser = Parser::HTTP1.new(self, .max_response_headers, .max_response_header_value_size) @buffer = buffer @version = [1, 1] @pending = [] @requests = [] @request = nil @handshake_completed = @pipelining = false end |
Instance Attribute Details
#max_concurrent_requests ⇒ Integer
Returns the value of attribute max_concurrent_requests.
21 22 23 |
# File 'lib/httpx/connection/http1.rb', line 21 def max_concurrent_requests @max_concurrent_requests end |
#pending ⇒ Array[Request] (readonly)
Returns the value of attribute pending.
19 20 21 |
# File 'lib/httpx/connection/http1.rb', line 19 def pending @pending end |
#requests ⇒ Array[Request] (readonly)
Returns the value of attribute requests.
19 20 21 |
# File 'lib/httpx/connection/http1.rb', line 19 def requests @requests end |
Instance Method Details
#<<(data) ⇒ void
This method returns an undefined value.
91 92 93 |
# File 'lib/httpx/connection/http1.rb', line 91 def <<(data) @parser << data end |
#capitalized(field) ⇒ String
412 413 414 |
# File 'lib/httpx/connection/http1.rb', line 412 def capitalized(field) UPCASED.fetch(field) { field.split("-").map(&:capitalize).join("-") } end |
#close ⇒ void
This method returns an undefined value.
71 72 73 74 |
# File 'lib/httpx/connection/http1.rb', line 71 def close reset emit(:close) end |
#consume ⇒ void
This method returns an undefined value.
107 108 109 110 111 112 113 114 115 116 |
# File 'lib/httpx/connection/http1.rb', line 107 def consume requests_limit = [@max_requests, @requests.size].min concurrent_requests_limit = [@max_concurrent_requests, requests_limit].min @requests.each_with_index do |request, idx| break if idx >= concurrent_requests_limit next unless request.can_buffer? handle(request) end end |
#disable ⇒ void
This method returns an undefined value.
293 294 295 296 297 298 |
# File 'lib/httpx/connection/http1.rb', line 293 def disable disable_pipelining reset emit(:reset) throw(:called) end |
#disable_pipelining ⇒ void
This method returns an undefined value.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/httpx/connection/http1.rb', line 300 def disable_pipelining # do not disable pipelining if already set to 1 request at a time return if @max_concurrent_requests == 1 @requests.each do |r| r.transition(:idle) if r.response.nil? # when we disable pipelining, we still want to try keep-alive. # only when keep-alive with one request fails, do we fallback to # connection: close. r.headers["connection"] = "close" if @max_concurrent_requests == 1 end # server doesn't handle pipelining, and probably # doesn't support keep-alive. Fallback to send only # 1 keep alive request. @max_concurrent_requests = 1 @pipelining = false end |
#dispatch(request) ⇒ void
This method returns an undefined value.
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/httpx/connection/http1.rb', line 182 def dispatch(request) if request.expects? @parser.reset! return handle(request) end @request = nil @requests.shift response = request.response emit(:response, request, response) if @parser.upgrade? response << @parser.upgrade_data @parser.reset! throw(:called) end @parser.reset! @max_requests -= 1 if response.is_a?(ErrorResponse) disable else manage_connection(request, response) end if exhausted? @pending.unshift(*@requests) @requests.clear emit(:exhausted) else send(@pending.shift) unless @pending.empty? end end |
#empty? ⇒ Boolean
80 81 82 83 84 85 86 87 88 89 |
# File 'lib/httpx/connection/http1.rb', line 80 def empty? # this means that for every request there's an available # partial response, so there are no in-flight requests waiting. @requests.empty? || ( # checking all responses can be time-consuming. Alas, as in HTTP/1, responses # do not come out of order, we can get away with checking first and last. !@requests.first.response.nil? && (@requests.size == 1 || !@requests.last.response.nil?) ) end |
#exhausted? ⇒ Boolean
76 77 78 |
# File 'lib/httpx/connection/http1.rb', line 76 def exhausted? !@max_requests.positive? end |
#handle(request) ⇒ void
This method returns an undefined value.
354 355 356 357 358 359 360 361 362 363 364 365 |
# File 'lib/httpx/connection/http1.rb', line 354 def handle(request) catch(:buffer_full) do request.transition(:headers) join_headers(request) if request.state == :headers request.transition(:body) join_body(request) if request.state == :body request.transition(:trailers) # HTTP/1.1 trailers should only work for chunked encoding join_trailers(request) if request.body.chunked? && request.state == :trailers request.transition(:done) end end |
#handle_error(ex, request = nil) ⇒ void
This method returns an undefined value.
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/httpx/connection/http1.rb', line 217 def handle_error(ex, request = nil) if (ex.is_a?(EOFError) || ex.is_a?(TimeoutError)) && @request && (response = @request.response) && response.is_a?(Response) && !response.headers.key?("content-length") && !response.headers.key?("transfer-encoding") # if the response does not contain a content-length header, the server closing the # connnection is the indicator of response consumed. # https://greenbytes.de/tech/webdav/rfc2616.html#rfc.section.4.4 catch(:called) { on_complete } return end if @pipelining catch(:called) { disable } else while (req = @requests.shift) next if request && request == req emit(:error, req, ex) end while (req = @pending.shift) next if request && request == req emit(:error, req, ex) end end end |
#interests ⇒ io_interests?
40 41 42 43 44 45 46 47 48 |
# File 'lib/httpx/connection/http1.rb', line 40 def interests request = @request || @requests.first return unless request return :w if request.interests == :w || !@buffer.empty? :r end |
#join_body(request) ⇒ void
This method returns an undefined value.
381 382 383 384 385 386 387 388 389 390 391 392 393 394 |
# File 'lib/httpx/connection/http1.rb', line 381 def join_body(request) return if request.body.empty? while (chunk = request.drain_body) request.log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." } request.log(level: 2, color: :green) { "<- #{log_redact_body(chunk.inspect)}" } @buffer << chunk throw(:buffer_full, request) if @buffer.full? end return unless (error = request.drain_error) raise error end |
#join_headers(request) ⇒ void
This method returns an undefined value.
371 372 373 374 375 376 377 378 379 |
# File 'lib/httpx/connection/http1.rb', line 371 def join_headers(request) headline = join_headline(request) @buffer << headline << CRLF request.log(color: :yellow) { "<- HEADLINE: #{headline.chomp.inspect}" } extra_headers = set_protocol_headers(request) join_headers2(request, request.headers.each(extra_headers)) request.log { "<- " } @buffer << CRLF end |
#join_headers2(request, headers) ⇒ void
This method returns an undefined value.
404 405 406 407 408 409 410 |
# File 'lib/httpx/connection/http1.rb', line 404 def join_headers2(request, headers) headers.each do |field, value| field = capitalized(field) request.log(color: :yellow) { "<- HEADER: #{[field, log_redact_headers(value)].join(": ")}" } @buffer << "#{field}: #{value}#{CRLF}" end end |
#join_headline(request) ⇒ String
367 368 369 |
# File 'lib/httpx/connection/http1.rb', line 367 def join_headline(request) "#{request.verb} #{request.path} HTTP/#{@version.join(".")}" end |
#join_trailers(request) ⇒ void
This method returns an undefined value.
396 397 398 399 400 401 402 |
# File 'lib/httpx/connection/http1.rb', line 396 def join_trailers(request) return unless request.trailers? && request.callbacks_for?(:trailers) join_headers2(request, request.trailers) request.log { "<- " } @buffer << CRLF end |
#manage_connection(request, response) ⇒ void
This method returns an undefined value.
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/httpx/connection/http1.rb', line 257 def manage_connection(request, response) connection = response.headers["connection"] case connection when /keep-alive/i if @handshake_completed if @max_requests.zero? @pending.unshift(*@requests) @requests.clear emit(:exhausted) end return end keep_alive = response.headers["keep-alive"] return unless keep_alive parameters = Hash[keep_alive.split(/ *, */).map do |pair| pair.split(/ *= */, 2) end] @max_requests = parameters["max"].to_i - 1 if parameters.key?("max") if parameters.key?("timeout") keep_alive_timeout = parameters["timeout"].to_i emit(:timeout, keep_alive_timeout) end @handshake_completed = true when /close/i disable when nil # In HTTP/1.1, it's keep alive by default return if response.version == "1.1" && request.headers["connection"] != "close" disable end end |
#on_complete ⇒ void
This method returns an undefined value.
173 174 175 176 177 178 179 180 |
# File 'lib/httpx/connection/http1.rb', line 173 def on_complete request = @request return unless request request.log(level: 2) { "parsing complete" } dispatch(request) end |
#on_data(chunk) ⇒ void
This method returns an undefined value.
160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/httpx/connection/http1.rb', line 160 def on_data(chunk) request = @request return unless request request.log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." } request.log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" } response = request.response response << chunk end |
#on_headers(h) ⇒ void
This method returns an undefined value.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/httpx/connection/http1.rb', line 126 def on_headers(h) request = @request = @requests.first return if request.response request.log(level: 2) { "headers received" } headers = request..headers_class.new(h) response = request..response_class.new(request, @parser.status_code, @parser.http_version.join("."), headers) request.log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" } request.log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v)}" }.join("\n") } if response.content_length && response.content_length > request..max_response_body_size raise HTTPX::Error, "maximum response body size exceeded" end request.response = response on_complete if response.finished? end |
#on_start ⇒ void
This method returns an undefined value.
HTTP Parser callbacks
must be public methods, or else they won't be reachable
122 123 124 |
# File 'lib/httpx/connection/http1.rb', line 122 def on_start log(level: 2) { "parsing begins" } end |
#on_trailers(h) ⇒ void
This method returns an undefined value.
148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/httpx/connection/http1.rb', line 148 def on_trailers(h) request = @request return unless request response = request.response request.log(level: 2) { "trailer headers received" } request.log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v.join(", "))}" }.join("\n") } response.merge_headers(h) end |
#ping ⇒ void
This method returns an undefined value.
245 246 247 248 249 |
# File 'lib/httpx/connection/http1.rb', line 245 def ping reset emit(:reset) emit(:exhausted) end |
#reset ⇒ void
This method returns an undefined value.
50 51 52 53 54 55 56 57 58 59 |
# File 'lib/httpx/connection/http1.rb', line 50 def reset if @ping_timer @ping_timer.cancel @ping_timer = nil end @max_requests = @options.max_requests || MAX_REQUESTS @parser.reset! @handshake_completed = false reset_requests end |
#reset_requests ⇒ void
This method returns an undefined value.
61 62 63 64 65 66 67 68 69 |
# File 'lib/httpx/connection/http1.rb', line 61 def reset_requests @requests.reverse_each do |request| next if request.response request.transition(:idle) @pending.unshift(request) end @requests.clear end |
#send(request) ⇒ void
This method returns an undefined value.
95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/httpx/connection/http1.rb', line 95 def send(request) unless @max_requests.positive? @pending << request return end return if @requests.include?(request) @requests << request @pipelining = @max_concurrent_requests > 1 && @requests.size > 1 end |
#set_protocol_headers(request) ⇒ _Each[[String, String]]
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 |
# File 'lib/httpx/connection/http1.rb', line 319 def set_protocol_headers(request) if !request.headers.key?("content-length") && request.body.bytesize == Float::INFINITY request.body.chunk! end extra_headers = {} unless request.headers.key?("connection") connection_value = if request.persistent? # when in a persistent connection, the request can't be at # the edge of a renegotiation if @requests.index(request) + 1 < @max_requests "keep-alive" else "close" end else # when it's not a persistent connection, it sets "Connection: close" always # on the last request of the possible batch (either allowed max requests, # or if smaller, the size of the batch itself) requests_limit = [@max_requests, @requests.size].min if request == @requests[requests_limit - 1] "close" else "keep-alive" end end extra_headers["connection"] = connection_value end extra_headers["host"] = request. unless request.headers.key?("host") extra_headers end |
#timeout ⇒ interval?
36 37 38 |
# File 'lib/httpx/connection/http1.rb', line 36 def timeout @options.timeout[:operation_timeout] end |
#waiting_for_ping? ⇒ Boolean
251 252 253 |
# File 'lib/httpx/connection/http1.rb', line 251 def waiting_for_ping? false end |