allow for oportunistic upgrades, such as the apache Upgrade: h2

this is achieved by a rework of the upgrade plugin, and the addition of
an h2 upgrade plugin. The idea is the following: if a response carries
an Upgrade header, and there's a handler for it, we should go for it.
The difference is:

* when the response is 101, this means that the negotiation must take
  place before the actual response comes in;
* when the response is 200, upgrading means reconnecting to the channel,
  and assume the new protocol for subsequent requests only.
This commit is contained in:
HoneyryderChuck 2021-03-02 18:58:44 +00:00
parent 72a397b841
commit a03e93e531
5 changed files with 114 additions and 13 deletions

View File

@ -472,14 +472,7 @@ module HTTPX
remove_instance_variable(:@total_timeout)
end
@io.close if @io
@read_buffer.clear
if @keep_alive_timer
@keep_alive_timer.cancel
remove_instance_variable(:@keep_alive_timer)
end
remove_instance_variable(:@timeout) if defined?(@timeout)
purge_after_closed
when :already_open
nextstate = :open
send_pending
@ -499,6 +492,17 @@ module HTTPX
emit(:close)
end
def purge_after_closed
@io.close if @io
@read_buffer.clear
if @keep_alive_timer
@keep_alive_timer.cancel
remove_instance_variable(:@keep_alive_timer)
end
remove_instance_variable(:@timeout) if defined?(@timeout)
end
def handle_response
@inflight -= 1
return unless @inflight.zero?

View File

@ -5,23 +5,31 @@ module HTTPX
module Upgrade
extend Registry
def self.load_dependencies(klass)
klass.plugin(:"upgrade/h2")
end
module InstanceMethods
def fetch_response(request, connections, options)
response = super
if response && response.status == 101
connection = find_connection(request, connections, options)
connections << connection unless connections.include?(connection)
if response && response.headers.key?("upgrade")
upgrade_protocol = (request.headers.get("upgrade") & response.headers.get("upgrade")).first
upgrade_protocol = response.headers["upgrade"].split(/ *, */).first
return response unless upgrade_protocol && Upgrade.registry.key?(upgrade_protocol)
protocol_handler = Upgrade.registry(upgrade_protocol)
return response unless protocol_handler
log { "upgrading to #{upgrade_protocol}..." }
connection = find_connection(request, connections, options)
connections << connection unless connections.include?(connection)
# do not upgrade already upgraded connections
return if connection.upgrade_protocol == upgrade_protocol
# TODO: exit it connection already upgraded?
protocol_handler.call(connection, request, response)
# keep in the loop if the server is switching, unless
@ -29,6 +37,7 @@ module HTTPX
# to terminante immediately
return if response.status == 101 && !connection.hijacked
end
response
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
module HTTPX
module Plugins
#
# This plugin adds support for upgrading a plaintext HTTP/1.1 connection to HTTP/2
# (https://tools.ietf.org/html/rfc7540#section-3.2)
#
# https://gitlab.com/honeyryderchuck/httpx/wikis/Follow-Redirects
#
module H2
class << self
def configure(*)
Upgrade.register "h2", self
end
def call(connection, _request, _response)
connection.upgrade_to_h2
end
end
module ConnectionMethods
using URIExtensions
def upgrade_to_h2
prev_parser = @parser
if prev_parser
prev_parser.reset
@inflight -= prev_parser.requests.size
end
@parser = Connection::HTTP2.new(@write_buffer, @options)
set_parser_callbacks(@parser)
@upgrade_protocol = :h2
# what's happening here:
# a deviation from the state machine is done to perform the actions when a
# connection is closed, without transitioning, so the connection is kept in the pool.
# the state is reset to initial, so that the socket reconnect works out of the box,
# while the parser is already here.
purge_after_closed
transition(:idle)
prev_parser.requests.each do |req|
req.transition(:idle)
send(req)
end
end
end
end
register_plugin(:"upgrade/h2", H2)
end
end

View File

@ -29,6 +29,7 @@ class HTTPSTest < Minitest::Test
include Plugins::Persistent unless RUBY_ENGINE == "jruby" || RUBY_VERSION < "2.3"
include Plugins::Stream
include Plugins::AWSAuthentication
include Plugins::Upgrade
def test_connection_coalescing
coalesced_origin = "https://#{ENV["HTTPBIN_COALESCING_HOST"]}"

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Requests
module Plugins
module Upgrade
def test_plugin_upgrade_h2
http = HTTPX.plugin(SessionWithPool)
if OpenSSL::SSL::SSLContext.instance_methods.include?(:alpn_protocols)
http = http.with(ssl: { alpn_protocols: %w[http/1.1] }) # disable alpn negotiation
end
http.plugin(:upgrade).wrap do |session|
uri = build_uri("/", "https://stadtschreiber.ruhr")
request = session.build_request(:get, uri)
request2 = session.build_request(:get, uri)
response = session.request(request)
verify_status(response, 200)
assert response.version == "1.1", "first request should be in HTTP/1.1"
response.close
# verifies that first request was used to upgrade the connection
verify_header(response.headers, "upgrade", "h2,h2c")
response2 = session.request(request2)
verify_status(response2, 200)
assert response2.version == "2.0", "second request should already be in HTTP/2"
response2.close
end
end
end
end
end