Skip to content

Commit 2f51d59

Browse files
committed
add Async::HTTP::Protocol::HTTP to auto-detect h1,h2 for inbound http:// connections
1 parent c58bafb commit 2f51d59

File tree

6 files changed

+94
-4
lines changed

6 files changed

+94
-4
lines changed

lib/async/http/protocol/http.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
# This detection requires a minimum number of bytes and is reliable for HTTP/1.1 and HTTP/2.
15+
# However, it can fail on HTTP/1.0 if the request is too small, such as path == '/' and no
16+
# headers. If you control the client (like a monitoring script), add a dummy header
17+
# or query value. Otherwise, use Async::HTTP::Protocol::HTTP1 instead.
18+
# Using a timeout on the Endpoint is strongly encouraged.
19+
module HTTP
20+
HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
21+
HTTP2_PREFACE_SIZE = HTTP2_PREFACE.bytesize
22+
23+
def self.protocol_for(stream)
24+
# Detect HTTP/2 connection preface
25+
# https://www.rfc-editor.org/rfc/rfc9113.html#section-3.4
26+
preface = stream.peek do |read_buffer|
27+
if read_buffer.bytesize >= HTTP2_PREFACE_SIZE
28+
break read_buffer[0, HTTP2_PREFACE_SIZE]
29+
end
30+
end
31+
if preface == HTTP2_PREFACE
32+
HTTP2
33+
else
34+
HTTP1
35+
end
36+
end
37+
38+
# Only inbound connections can detect HTTP1 vs HTTP2 for http://.
39+
# Outbound connections default to HTTP1.
40+
def self.client(peer, **kwargs)
41+
HTTP1.client(peer, **kwargs)
42+
end
43+
44+
def self.server(peer, **kwargs)
45+
stream = IO::Stream.new(peer, sync: true)
46+
protocol_for(stream).server(stream, **kwargs)
47+
end
48+
49+
def self.names
50+
["h2", "http/1.1", "http/1.0"]
51+
end
52+
53+
end
54+
end
55+
end
56+
end

lib/async/http/protocol/http1.rb

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

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

67
require_relative 'http1/client'
78
require_relative 'http1/server'
@@ -27,7 +28,7 @@ def self.client(peer)
2728
end
2829

2930
def self.server(peer)
30-
stream = IO::Stream.new(peer, sync: true)
31+
stream = peer.is_a?(IO::Stream) ? peer : IO::Stream.new(peer, sync: true)
3132

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

lib/async/http/protocol/http10.rb

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

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

67
require_relative 'http1'
78

@@ -26,7 +27,7 @@ def self.client(peer)
2627
end
2728

2829
def self.server(peer)
29-
stream = IO::Stream.new(peer, sync: true)
30+
stream = peer.is_a?(IO::Stream) ? peer : IO::Stream.new(peer, sync: true)
3031

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

lib/async/http/protocol/http11.rb

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

78
require_relative 'http1'
89

@@ -27,7 +28,7 @@ def self.client(peer)
2728
end
2829

2930
def self.server(peer)
30-
stream = IO::Stream.new(peer, sync: true)
31+
stream = peer.is_a?(IO::Stream) ? peer : IO::Stream.new(peer, sync: true)
3132

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

lib/async/http/protocol/http2.rb

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

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

67
require_relative 'http2/client'
78
require_relative 'http2/server'
@@ -46,7 +47,7 @@ def self.client(peer, settings = CLIENT_SETTINGS)
4647
end
4748

4849
def self.server(peer, settings = SERVER_SETTINGS)
49-
stream = IO::Stream.new(peer, sync: true)
50+
stream = peer.is_a?(IO::Stream) ? peer : IO::Stream.new(peer, sync: true)
5051

5152
server = Server.new(stream)
5253

spec/async/http/protocol/http_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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_relative '../server_context'
8+
9+
RSpec.describe Async::HTTP::Protocol::HTTP do
10+
include_context Async::HTTP::Server
11+
12+
context 'http11 client' do
13+
it "should make a successful request" do |example|
14+
response = client.get("/")
15+
expect(response).to be_success
16+
expect(response.version).to be == 'HTTP/1.1'
17+
end
18+
end
19+
20+
context 'http2 client' do
21+
let(:client_endpoint) {endpoint.with(protocol: Async::HTTP::Protocol::HTTP2)}
22+
23+
it "should make a successful request" do |example|
24+
response = client.get("/")
25+
expect(response).to be_success
26+
expect(response.version).to be == 'HTTP/2'
27+
response.read
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)