Skip to content

Commit 1e4fc00

Browse files
authored
Add auto-detection of HTTP1 vs HTTP2 for inbound connections. (#128)
1 parent 019310e commit 1e4fc00

File tree

8 files changed

+109
-13
lines changed

8 files changed

+109
-13
lines changed

lib/async/http/endpoint.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
require 'io/endpoint/host_endpoint'
99
require 'io/endpoint/ssl_endpoint'
1010

11-
require_relative 'protocol/http1'
11+
require_relative 'protocol/http'
1212
require_relative 'protocol/https'
1313

1414
module Async
@@ -84,7 +84,7 @@ def protocol
8484
if secure?
8585
Protocol::HTTPS
8686
else
87-
Protocol::HTTP1
87+
Protocol::HTTP
8888
end
8989
end
9090
end

lib/async/http/protocol/http.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2023, by Thomas Morgan.
5+
6+
require_relative 'http1'
7+
require_relative 'http2'
8+
9+
module Async
10+
module HTTP
11+
module Protocol
12+
# HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2
13+
# connection preface.
14+
module HTTP
15+
HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
16+
HTTP2_PREFACE_SIZE = HTTP2_PREFACE.bytesize
17+
18+
def self.protocol_for(stream)
19+
# Detect HTTP/2 connection preface
20+
# https://www.rfc-editor.org/rfc/rfc9113.html#section-3.4
21+
preface = stream.peek do |read_buffer|
22+
if read_buffer.bytesize >= HTTP2_PREFACE_SIZE
23+
break read_buffer[0, HTTP2_PREFACE_SIZE]
24+
elsif read_buffer.bytesize > 0
25+
# If partial read_buffer already doesn't match, no need to wait for more bytes.
26+
break read_buffer unless HTTP2_PREFACE[read_buffer]
27+
end
28+
end
29+
30+
if preface == HTTP2_PREFACE
31+
HTTP2
32+
else
33+
HTTP1
34+
end
35+
end
36+
37+
# Only inbound connections can detect HTTP1 vs HTTP2 for http://.
38+
# Outbound connections default to HTTP1.
39+
def self.client(peer, **options)
40+
HTTP1.client(peer, **options)
41+
end
42+
43+
def self.server(peer, **options)
44+
stream = ::IO::Stream(peer)
45+
46+
return protocol_for(stream).server(stream, **options)
47+
end
48+
49+
def self.names
50+
["h2", "http/1.1", "http/1.0"]
51+
end
52+
end
53+
end
54+
end
55+
end

lib/async/http/protocol/http1.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
# Released under the MIT License.
44
# Copyright, 2017-2024, by Samuel Williams.
5+
# Copyright, 2023, by Thomas Morgan.
56

67
require_relative 'http1/client'
78
require_relative 'http1/server'
89

9-
require 'io/stream/buffered'
10+
require 'io/stream'
1011

1112
module Async
1213
module HTTP
@@ -23,13 +24,13 @@ def self.trailer?
2324
end
2425

2526
def self.client(peer)
26-
stream = ::IO::Stream::Buffered.wrap(peer)
27+
stream = ::IO::Stream(peer)
2728

2829
return HTTP1::Client.new(stream, VERSION)
2930
end
3031

3132
def self.server(peer)
32-
stream = ::IO::Stream::Buffered.wrap(peer)
33+
stream = ::IO::Stream(peer)
3334

3435
return HTTP1::Server.new(stream, VERSION)
3536
end

lib/async/http/protocol/http10.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# Released under the MIT License.
44
# Copyright, 2017-2024, by Samuel Williams.
5+
# Copyright, 2023, by Thomas Morgan.
56

67
require_relative 'http1'
78

@@ -20,13 +21,13 @@ def self.trailer?
2021
end
2122

2223
def self.client(peer)
23-
stream = ::IO::Stream::Buffered.wrap(peer)
24+
stream = ::IO::Stream(peer)
2425

2526
return HTTP1::Client.new(stream, VERSION)
2627
end
2728

2829
def self.server(peer)
29-
stream = ::IO::Stream::Buffered.wrap(peer)
30+
stream = ::IO::Stream(peer)
3031

3132
return HTTP1::Server.new(stream, VERSION)
3233
end

lib/async/http/protocol/http11.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# Released under the MIT License.
44
# Copyright, 2017-2024, by Samuel Williams.
55
# Copyright, 2018, by Janko Marohnić.
6+
# Copyright, 2023, by Thomas Morgan.
67

78
require_relative 'http1'
89

@@ -21,13 +22,13 @@ def self.trailer?
2122
end
2223

2324
def self.client(peer)
24-
stream = ::IO::Stream::Buffered.wrap(peer)
25+
stream = ::IO::Stream(peer)
2526

2627
return HTTP1::Client.new(stream, VERSION)
2728
end
2829

2930
def self.server(peer)
30-
stream = ::IO::Stream::Buffered.wrap(peer)
31+
stream = ::IO::Stream(peer)
3132

3233
return HTTP1::Server.new(stream, VERSION)
3334
end

lib/async/http/protocol/http2.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
# Released under the MIT License.
44
# Copyright, 2018-2024, by Samuel Williams.
5+
# Copyright, 2023, by Thomas Morgan.
56

67
require_relative 'http2/client'
78
require_relative 'http2/server'
89

9-
require 'io/stream/buffered'
10+
require 'io/stream'
1011

1112
module Async
1213
module HTTP
@@ -37,7 +38,7 @@ def self.trailer?
3738
}
3839

3940
def self.client(peer, settings = CLIENT_SETTINGS)
40-
stream = ::IO::Stream::Buffered.wrap(peer)
41+
stream = ::IO::Stream(peer)
4142
client = Client.new(stream)
4243

4344
client.send_connection_preface(settings)
@@ -47,7 +48,7 @@ def self.client(peer, settings = CLIENT_SETTINGS)
4748
end
4849

4950
def self.server(peer, settings = SERVER_SETTINGS)
50-
stream = ::IO::Stream::Buffered.wrap(peer)
51+
stream = ::IO::Stream(peer)
5152
server = Server.new(stream)
5253

5354
server.read_connection_preface(settings)

test/async/http/endpoint.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@
155155

156156
describe Async::HTTP::Endpoint.parse("http://www.google.com/search") do
157157
it "should select the correct protocol" do
158-
expect(subject.protocol).to be == Async::HTTP::Protocol::HTTP1
158+
expect(subject.protocol).to be == Async::HTTP::Protocol::HTTP
159159
end
160160

161161
it "should parse the correct hostname" do

test/async/http/protocol/http.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2023, by Thomas Morgan.
5+
6+
require 'async/http/protocol/http'
7+
require 'async/http/a_protocol'
8+
9+
describe Async::HTTP::Protocol::HTTP do
10+
with 'server' do
11+
include Sus::Fixtures::Async::HTTP::ServerContext
12+
let(:protocol) {subject}
13+
14+
with 'http11 client' do
15+
it 'should make a successful request' do
16+
response = client.get('/')
17+
expect(response).to be(:success?)
18+
expect(response.version).to be == 'HTTP/1.1'
19+
response.read
20+
end
21+
end
22+
23+
with 'http2 client' do
24+
def make_client(endpoint, **options)
25+
options[:protocol] = Async::HTTP::Protocol::HTTP2
26+
super
27+
end
28+
29+
it 'should make a successful request' do
30+
response = client.get('/')
31+
expect(response).to be(:success?)
32+
expect(response.version).to be == 'HTTP/2'
33+
response.read
34+
end
35+
end
36+
end
37+
end

0 commit comments

Comments
 (0)