async-http 0.87.0 → 0.89.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +2 -3
- data/bake/async/http/h2spec.rb +2 -2
- data/lib/async/http/client.rb +3 -3
- data/lib/async/http/endpoint.rb +2 -2
- data/lib/async/http/protocol/configurable.rb +43 -0
- data/lib/async/http/protocol/defaulton.rb +36 -0
- data/lib/async/http/protocol/http.rb +39 -17
- data/lib/async/http/protocol/http1/client.rb +0 -2
- data/lib/async/http/protocol/http1/connection.rb +4 -3
- data/lib/async/http/protocol/http1/server.rb +4 -3
- data/lib/async/http/protocol/http1.rb +20 -5
- data/lib/async/http/protocol/http10.rb +18 -5
- data/lib/async/http/protocol/http11.rb +18 -5
- data/lib/async/http/protocol/http2/client.rb +1 -3
- data/lib/async/http/protocol/http2/connection.rb +5 -5
- data/lib/async/http/protocol/http2/server.rb +1 -3
- data/lib/async/http/protocol/http2/stream.rb +2 -2
- data/lib/async/http/protocol/http2.rb +20 -3
- data/lib/async/http/protocol/https.rb +46 -12
- data/lib/async/http/server.rb +3 -3
- data/lib/async/http/version.rb +2 -2
- data/license.md +1 -0
- data/readme.md +4 -4
- data/releases.md +57 -0
- data.tar.gz.sig +0 -0
- metadata +6 -3
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b240b006b75c0a01b07afc6245603c28e272d47f6a5cc2beeb4af62637733a5
|
4
|
+
data.tar.gz: 6a9e111b0845c065e812c71232dac7ccde12b4c3e4cfa0f74feec21c1bdac331
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0528d35edbb15f8685c54168c79857f4554e75ae5e1f866ae61fc3f407268211d3ecd342ee3afc61d6c320da03affd1f4f7a98d434b75eb2cad6a5a6667e3204'
|
7
|
+
data.tar.gz: 906ea0d679469fbebce890bad4c04c57b94b9f5e0218c113dfd2b05c1be99cb9ff9285e49b1d064582219af1ccd6150defb0622477d3fea4773cf3547f9cdaee
|
checksums.yaml.gz.sig
CHANGED
@@ -1,3 +1,2 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
MU�
|
1
|
+
���O�Y�����a���k��~�.aV�����$DarB��������m��
|
2
|
+
Y")����Q�����X/��V�'f��U���Yy��e*��C�1��@��:+��E&�*�A�e�4 �
|
data/bake/async/http/h2spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
def build
|
7
7
|
# Fetch the code:
|
@@ -30,7 +30,7 @@ def server
|
|
30
30
|
|
31
31
|
container = Async::Container.new
|
32
32
|
|
33
|
-
Console.
|
33
|
+
Console.info(self){"Starting server..."}
|
34
34
|
|
35
35
|
container.run(count: 1) do
|
36
36
|
server = Async::HTTP::Server.for(endpoint, protocol: Async::HTTP::Protocol::HTTP2, scheme: "https") do |request|
|
data/lib/async/http/client.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2022, by Ian Ker-Seymer.
|
6
6
|
|
7
7
|
require "io/endpoint"
|
@@ -81,7 +81,7 @@ module Async
|
|
81
81
|
|
82
82
|
def close
|
83
83
|
while @pool.busy?
|
84
|
-
Console.
|
84
|
+
Console.warn(self) {"Waiting for #{@protocol} pool to drain: #{@pool}"}
|
85
85
|
@pool.wait
|
86
86
|
end
|
87
87
|
|
@@ -164,7 +164,7 @@ module Async
|
|
164
164
|
self.assign_default_tags(options[:tags] ||= {})
|
165
165
|
|
166
166
|
Async::Pool::Controller.wrap(**options) do
|
167
|
-
Console.
|
167
|
+
Console.debug(self) {"Making connection to #{@endpoint.inspect}"}
|
168
168
|
|
169
169
|
@protocol.client(@endpoint.connect)
|
170
170
|
end
|
data/lib/async/http/endpoint.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2021-2022, by Adam Daniels.
|
6
6
|
# Copyright, 2024, by Thomas Morgan.
|
7
7
|
# Copyright, 2024, by Igor Sidorov.
|
@@ -40,7 +40,7 @@ module Async
|
|
40
40
|
#
|
41
41
|
# @parameter scheme [String] The scheme to use, e.g. "http" or "https".
|
42
42
|
# @parameter hostname [String] The hostname to connect to (or bind to).
|
43
|
-
# @parameter *options [Hash] Additional options, passed to {
|
43
|
+
# @parameter *options [Hash] Additional options, passed to {initialize}.
|
44
44
|
def self.for(scheme, hostname, path = "/", **options)
|
45
45
|
# TODO: Consider using URI.for once it becomes available:
|
46
46
|
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
module Async
|
7
|
+
module HTTP
|
8
|
+
module Protocol
|
9
|
+
class Configured
|
10
|
+
def initialize(protocol, **options)
|
11
|
+
@protocol = protocol
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# @attribute [Protocol] The underlying protocol.
|
16
|
+
attr :protocol
|
17
|
+
|
18
|
+
# @attribute [Hash] The options to pass to the protocol.
|
19
|
+
attr :options
|
20
|
+
|
21
|
+
def client(peer, **options)
|
22
|
+
options = @options.merge(options)
|
23
|
+
@protocol.client(peer, **options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def server(peer, **options)
|
27
|
+
options = @options.merge(options)
|
28
|
+
@protocol.server(peer, **options)
|
29
|
+
end
|
30
|
+
|
31
|
+
def names
|
32
|
+
@protocol.names
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module Configurable
|
37
|
+
def new(**options)
|
38
|
+
Configured.new(self, **options)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
module Async
|
7
|
+
module HTTP
|
8
|
+
module Protocol
|
9
|
+
# This module provides a default instance of the protocol, which can be used to create clients and servers. The name is a play on "Default" + "Singleton".
|
10
|
+
module Defaulton
|
11
|
+
def self.extended(base)
|
12
|
+
base.instance_variable_set(:@default, base.new)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :default
|
16
|
+
|
17
|
+
# Create a client for an outbound connection, using the default instance.
|
18
|
+
def client(peer, **options)
|
19
|
+
default.client(peer, **options)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Create a server for an inbound connection, using the default instance.
|
23
|
+
def server(peer, **options)
|
24
|
+
default.server(peer, **options)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @returns [Array] The names of the supported protocol, used for Application Layer Protocol Negotiation (ALPN), using the default instance.
|
28
|
+
def names
|
29
|
+
default.names
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private_constant :Defaulton
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -2,7 +2,9 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2024, by Thomas Morgan.
|
5
|
-
# Copyright, 2024, by Samuel Williams.
|
5
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
6
|
+
|
7
|
+
require_relative "defaulton"
|
6
8
|
|
7
9
|
require_relative "http1"
|
8
10
|
require_relative "http2"
|
@@ -10,13 +12,25 @@ require_relative "http2"
|
|
10
12
|
module Async
|
11
13
|
module HTTP
|
12
14
|
module Protocol
|
13
|
-
# HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2
|
14
|
-
|
15
|
-
module HTTP
|
15
|
+
# HTTP is an http:// server that auto-selects HTTP/1.1 or HTTP/2 by detecting the HTTP/2 connection preface.
|
16
|
+
class HTTP
|
16
17
|
HTTP2_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
|
17
18
|
HTTP2_PREFACE_SIZE = HTTP2_PREFACE.bytesize
|
18
19
|
|
19
|
-
|
20
|
+
# Create a new HTTP protocol instance.
|
21
|
+
#
|
22
|
+
# @parameter http1 [HTTP1] The HTTP/1 protocol instance.
|
23
|
+
# @parameter http2 [HTTP2] The HTTP/2 protocol instance.
|
24
|
+
def initialize(http1: HTTP1, http2: HTTP2)
|
25
|
+
@http1 = http1
|
26
|
+
@http2 = http2
|
27
|
+
end
|
28
|
+
|
29
|
+
# Determine if the inbound connection is HTTP/1 or HTTP/2.
|
30
|
+
#
|
31
|
+
# @parameter stream [IO::Stream] The stream to detect the protocol for.
|
32
|
+
# @returns [Class] The protocol class to use.
|
33
|
+
def protocol_for(stream)
|
20
34
|
# Detect HTTP/2 connection preface
|
21
35
|
# https://www.rfc-editor.org/rfc/rfc9113.html#section-3.4
|
22
36
|
preface = stream.peek do |read_buffer|
|
@@ -29,27 +43,35 @@ module Async
|
|
29
43
|
end
|
30
44
|
|
31
45
|
if preface == HTTP2_PREFACE
|
32
|
-
|
46
|
+
@http2
|
33
47
|
else
|
34
|
-
|
48
|
+
@http1
|
35
49
|
end
|
36
50
|
end
|
37
51
|
|
38
|
-
#
|
39
|
-
#
|
40
|
-
|
41
|
-
|
52
|
+
# Create a client for an outbound connection. Defaults to HTTP/1 for plaintext connections.
|
53
|
+
#
|
54
|
+
# @parameter peer [IO] The peer to communicate with.
|
55
|
+
# @parameter options [Hash] Options to pass to the protocol, keyed by protocol class.
|
56
|
+
def client(peer, **options)
|
57
|
+
options = options[@http1] || {}
|
58
|
+
|
59
|
+
return @http1.client(peer, **options)
|
42
60
|
end
|
43
61
|
|
44
|
-
|
45
|
-
|
62
|
+
# Create a server for an inbound connection. Able to detect HTTP1 and HTTP2.
|
63
|
+
#
|
64
|
+
# @parameter peer [IO] The peer to communicate with.
|
65
|
+
# @parameter options [Hash] Options to pass to the protocol, keyed by protocol class.
|
66
|
+
def server(peer, **options)
|
67
|
+
stream = IO::Stream(peer)
|
68
|
+
protocol = protocol_for(stream)
|
69
|
+
options = options[protocol] || {}
|
46
70
|
|
47
|
-
return
|
71
|
+
return protocol.server(stream, **options)
|
48
72
|
end
|
49
73
|
|
50
|
-
|
51
|
-
["h2", "http/1.1", "http/1.0"]
|
52
|
-
end
|
74
|
+
extend Defaulton
|
53
75
|
end
|
54
76
|
end
|
55
77
|
end
|
@@ -32,8 +32,6 @@ module Async
|
|
32
32
|
|
33
33
|
# Used by the client to send requests to the remote server.
|
34
34
|
def call(request, task: Task.current)
|
35
|
-
Console.logger.debug(self) {"#{request.method} #{request.path} #{request.headers.inspect}"}
|
36
|
-
|
37
35
|
# Mark the start of the trailers:
|
38
36
|
trailer = request.headers.trailer!
|
39
37
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "request"
|
7
7
|
require_relative "response"
|
@@ -14,9 +14,10 @@ module Async
|
|
14
14
|
module Protocol
|
15
15
|
module HTTP1
|
16
16
|
class Connection < ::Protocol::HTTP1::Connection
|
17
|
-
def initialize(stream, version)
|
18
|
-
super(stream)
|
17
|
+
def initialize(stream, version, **options)
|
18
|
+
super(stream, **options)
|
19
19
|
|
20
|
+
# On the client side, we need to send the HTTP version with the initial request. On the server side, there are some scenarios (bad request) where we don't know the request version. In those cases, we use this value, which is either hard coded based on the protocol being used, OR could be negotiated during the connection setup (e.g. ALPN).
|
20
21
|
@version = version
|
21
22
|
end
|
22
23
|
|
@@ -1,10 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Igor Sidorov.
|
6
6
|
# Copyright, 2023, by Thomas Morgan.
|
7
7
|
# Copyright, 2024, by Anton Zhuravsky.
|
8
|
+
# Copyright, 2025, by Jean Boussier.
|
8
9
|
|
9
10
|
require_relative "connection"
|
10
11
|
require_relative "finishable"
|
@@ -34,7 +35,7 @@ module Async
|
|
34
35
|
write_body(@version, nil)
|
35
36
|
rescue => error
|
36
37
|
# At this point, there is very little we can do to recover:
|
37
|
-
Console
|
38
|
+
Console.debug(self, "Failed to write failure response!", error)
|
38
39
|
end
|
39
40
|
|
40
41
|
def next_request
|
@@ -52,7 +53,7 @@ module Async
|
|
52
53
|
end
|
53
54
|
|
54
55
|
return request
|
55
|
-
rescue ::Protocol::HTTP1::BadRequest
|
56
|
+
rescue ::Protocol::HTTP1::BadRequest
|
56
57
|
fail_request(400)
|
57
58
|
# Conceivably we could retry here, but we don't really know how bad the error is, so it's better to just fail:
|
58
59
|
raise
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2024, by Thomas Morgan.
|
6
6
|
|
7
|
+
require_relative "configurable"
|
8
|
+
|
7
9
|
require_relative "http1/client"
|
8
10
|
require_relative "http1/server"
|
9
11
|
|
@@ -13,28 +15,41 @@ module Async
|
|
13
15
|
module HTTP
|
14
16
|
module Protocol
|
15
17
|
module HTTP1
|
18
|
+
extend Configurable
|
19
|
+
|
16
20
|
VERSION = "HTTP/1.1"
|
17
21
|
|
22
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
18
23
|
def self.bidirectional?
|
19
24
|
true
|
20
25
|
end
|
21
26
|
|
27
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
22
28
|
def self.trailer?
|
23
29
|
true
|
24
30
|
end
|
25
31
|
|
26
|
-
|
32
|
+
# Create a client for an outbound connection.
|
33
|
+
#
|
34
|
+
# @parameter peer [IO] The peer to communicate with.
|
35
|
+
# @parameter options [Hash] Options to pass to the client instance.
|
36
|
+
def self.client(peer, **options)
|
27
37
|
stream = ::IO::Stream(peer)
|
28
38
|
|
29
|
-
return HTTP1::Client.new(stream, VERSION)
|
39
|
+
return HTTP1::Client.new(stream, VERSION, **options)
|
30
40
|
end
|
31
41
|
|
32
|
-
|
42
|
+
# Create a server for an inbound connection.
|
43
|
+
#
|
44
|
+
# @parameter peer [IO] The peer to communicate with.
|
45
|
+
# @parameter options [Hash] Options to pass to the server instance.
|
46
|
+
def self.server(peer, **options)
|
33
47
|
stream = ::IO::Stream(peer)
|
34
48
|
|
35
|
-
return HTTP1::Server.new(stream, VERSION)
|
49
|
+
return HTTP1::Server.new(stream, VERSION, **options)
|
36
50
|
end
|
37
51
|
|
52
|
+
# @returns [Array] The names of the supported protocol.
|
38
53
|
def self.names
|
39
54
|
["http/1.1", "http/1.0"]
|
40
55
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2024, by Thomas Morgan.
|
6
6
|
|
7
7
|
require_relative "http1"
|
@@ -10,28 +10,41 @@ module Async
|
|
10
10
|
module HTTP
|
11
11
|
module Protocol
|
12
12
|
module HTTP10
|
13
|
+
extend Configurable
|
14
|
+
|
13
15
|
VERSION = "HTTP/1.0"
|
14
16
|
|
17
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
15
18
|
def self.bidirectional?
|
16
19
|
false
|
17
20
|
end
|
18
21
|
|
22
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
19
23
|
def self.trailer?
|
20
24
|
false
|
21
25
|
end
|
22
26
|
|
23
|
-
|
27
|
+
# Create a client for an outbound connection.
|
28
|
+
#
|
29
|
+
# @parameter peer [IO] The peer to communicate with.
|
30
|
+
# @parameter options [Hash] Options to pass to the client instance.
|
31
|
+
def self.client(peer, **options)
|
24
32
|
stream = ::IO::Stream(peer)
|
25
33
|
|
26
|
-
return HTTP1::Client.new(stream, VERSION)
|
34
|
+
return HTTP1::Client.new(stream, VERSION, **options)
|
27
35
|
end
|
28
36
|
|
29
|
-
|
37
|
+
# Create a server for an inbound connection.
|
38
|
+
#
|
39
|
+
# @parameter peer [IO] The peer to communicate with.
|
40
|
+
# @parameter options [Hash] Options to pass to the server instance.
|
41
|
+
def self.server(peer, **options)
|
30
42
|
stream = ::IO::Stream(peer)
|
31
43
|
|
32
|
-
return HTTP1::Server.new(stream, VERSION)
|
44
|
+
return HTTP1::Server.new(stream, VERSION, **options)
|
33
45
|
end
|
34
46
|
|
47
|
+
# @returns [Array] The names of the supported protocol.
|
35
48
|
def self.names
|
36
49
|
["http/1.0"]
|
37
50
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2018, by Janko Marohnić.
|
6
6
|
# Copyright, 2024, by Thomas Morgan.
|
7
7
|
|
@@ -11,28 +11,41 @@ module Async
|
|
11
11
|
module HTTP
|
12
12
|
module Protocol
|
13
13
|
module HTTP11
|
14
|
+
extend Configurable
|
15
|
+
|
14
16
|
VERSION = "HTTP/1.1"
|
15
17
|
|
18
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
16
19
|
def self.bidirectional?
|
17
20
|
true
|
18
21
|
end
|
19
22
|
|
23
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
20
24
|
def self.trailer?
|
21
25
|
true
|
22
26
|
end
|
23
27
|
|
24
|
-
|
28
|
+
# Create a client for an outbound connection.
|
29
|
+
#
|
30
|
+
# @parameter peer [IO] The peer to communicate with.
|
31
|
+
# @parameter options [Hash] Options to pass to the client instance.
|
32
|
+
def self.client(peer, **options)
|
25
33
|
stream = ::IO::Stream(peer)
|
26
34
|
|
27
|
-
return HTTP1::Client.new(stream, VERSION)
|
35
|
+
return HTTP1::Client.new(stream, VERSION, **options)
|
28
36
|
end
|
29
37
|
|
30
|
-
|
38
|
+
# Create a server for an inbound connection.
|
39
|
+
#
|
40
|
+
# @parameter peer [IO] The peer to communicate with.
|
41
|
+
# @parameter options [Hash] Options to pass to the server instance.
|
42
|
+
def self.server(peer, **options)
|
31
43
|
stream = ::IO::Stream(peer)
|
32
44
|
|
33
|
-
return HTTP1::Server.new(stream, VERSION)
|
45
|
+
return HTTP1::Server.new(stream, VERSION, **options)
|
34
46
|
end
|
35
47
|
|
48
|
+
# @returns [Array] The names of the supported protocol.
|
36
49
|
def self.names
|
37
50
|
["http/1.1"]
|
38
51
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "connection"
|
7
7
|
require_relative "response"
|
@@ -32,8 +32,6 @@ module Async
|
|
32
32
|
def call(request)
|
33
33
|
raise ::Protocol::HTTP2::Error, "Connection closed!" if self.closed?
|
34
34
|
|
35
|
-
@count += 1
|
36
|
-
|
37
35
|
response = create_response
|
38
36
|
write_request(response, request)
|
39
37
|
read_response(response)
|
@@ -1,8 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2020, by Bruno Sutic.
|
6
|
+
# Copyright, 2025, by Jean Boussier.
|
6
7
|
|
7
8
|
require_relative "stream"
|
8
9
|
|
@@ -26,10 +27,9 @@ module Async
|
|
26
27
|
TRAILER = "trailer".freeze
|
27
28
|
|
28
29
|
module Connection
|
29
|
-
def initialize(
|
30
|
+
def initialize(...)
|
30
31
|
super
|
31
32
|
|
32
|
-
@count = 0
|
33
33
|
@reader = nil
|
34
34
|
|
35
35
|
# Writing multiple frames at the same time can cause odd problems if frames are only partially written. So we use a semaphore to ensure frames are written in their entirety.
|
@@ -41,7 +41,7 @@ module Async
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def to_s
|
44
|
-
"\#<#{self.class} #{@
|
44
|
+
"\#<#{self.class} #{@streams.count} active streams>"
|
45
45
|
end
|
46
46
|
|
47
47
|
def as_json(...)
|
@@ -95,7 +95,7 @@ module Async
|
|
95
95
|
# Error is raised if a response is actively reading from the
|
96
96
|
# connection. The connection is silently closed if GOAWAY is
|
97
97
|
# received outside the request/response cycle.
|
98
|
-
rescue SocketError, IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE
|
98
|
+
rescue SocketError, IOError, EOFError, Errno::ECONNRESET, Errno::EPIPE
|
99
99
|
# Ignore.
|
100
100
|
rescue => error
|
101
101
|
# Every other error.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "connection"
|
7
7
|
require_relative "request"
|
@@ -51,8 +51,6 @@ module Async
|
|
51
51
|
@requests&.async do |task, request|
|
52
52
|
task.annotate("Incoming request: #{request.method} #{request.path.inspect}.")
|
53
53
|
|
54
|
-
@count += 1
|
55
|
-
|
56
54
|
task.defer_stop do
|
57
55
|
response = yield(request)
|
58
56
|
rescue
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2022, by Marco Concetto Rudilosso.
|
6
6
|
# Copyright, 2023, by Thomas Morgan.
|
7
7
|
|
@@ -65,7 +65,7 @@ module Async
|
|
65
65
|
@input.close_write
|
66
66
|
end
|
67
67
|
rescue ::Protocol::HTTP2::HeaderError => error
|
68
|
-
Console.
|
68
|
+
Console.debug(self, "Error while processing headers!", error)
|
69
69
|
|
70
70
|
send_reset_stream(error.code)
|
71
71
|
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2024, by Thomas Morgan.
|
6
6
|
|
7
|
+
require_relative "configurable"
|
8
|
+
|
7
9
|
require_relative "http2/client"
|
8
10
|
require_relative "http2/server"
|
9
11
|
|
@@ -13,16 +15,21 @@ module Async
|
|
13
15
|
module HTTP
|
14
16
|
module Protocol
|
15
17
|
module HTTP2
|
18
|
+
extend Configurable
|
19
|
+
|
16
20
|
VERSION = "HTTP/2"
|
17
21
|
|
22
|
+
# @returns [Boolean] Whether the protocol supports bidirectional communication.
|
18
23
|
def self.bidirectional?
|
19
24
|
true
|
20
25
|
end
|
21
26
|
|
27
|
+
# @returns [Boolean] Whether the protocol supports trailers.
|
22
28
|
def self.trailer?
|
23
29
|
true
|
24
30
|
end
|
25
31
|
|
32
|
+
# The default settings for the client.
|
26
33
|
CLIENT_SETTINGS = {
|
27
34
|
::Protocol::HTTP2::Settings::ENABLE_PUSH => 0,
|
28
35
|
::Protocol::HTTP2::Settings::MAXIMUM_FRAME_SIZE => 0x100000,
|
@@ -30,6 +37,7 @@ module Async
|
|
30
37
|
::Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES => 1,
|
31
38
|
}
|
32
39
|
|
40
|
+
# The default settings for the server.
|
33
41
|
SERVER_SETTINGS = {
|
34
42
|
# We choose a lower maximum concurrent streams to avoid overloading a single connection/thread.
|
35
43
|
::Protocol::HTTP2::Settings::MAXIMUM_CONCURRENT_STREAMS => 128,
|
@@ -39,7 +47,11 @@ module Async
|
|
39
47
|
::Protocol::HTTP2::Settings::NO_RFC7540_PRIORITIES => 1,
|
40
48
|
}
|
41
49
|
|
42
|
-
|
50
|
+
# Create a client for an outbound connection.
|
51
|
+
#
|
52
|
+
# @parameter peer [IO] The peer to communicate with.
|
53
|
+
# @parameter options [Hash] Options to pass to the client instance.
|
54
|
+
def self.client(peer, settings: CLIENT_SETTINGS)
|
43
55
|
stream = ::IO::Stream(peer)
|
44
56
|
client = Client.new(stream)
|
45
57
|
|
@@ -49,7 +61,11 @@ module Async
|
|
49
61
|
return client
|
50
62
|
end
|
51
63
|
|
52
|
-
|
64
|
+
# Create a server for an inbound connection.
|
65
|
+
#
|
66
|
+
# @parameter peer [IO] The peer to communicate with.
|
67
|
+
# @parameter options [Hash] Options to pass to the server instance.
|
68
|
+
def self.server(peer, settings: SERVER_SETTINGS)
|
53
69
|
stream = ::IO::Stream(peer)
|
54
70
|
server = Server.new(stream)
|
55
71
|
|
@@ -59,6 +75,7 @@ module Async
|
|
59
75
|
return server
|
60
76
|
end
|
61
77
|
|
78
|
+
# @returns [Array] The names of the supported protocol.
|
62
79
|
def self.names
|
63
80
|
["h2"]
|
64
81
|
end
|
@@ -1,19 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2018-
|
4
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2019, by Brian Morearty.
|
6
6
|
|
7
|
+
require_relative "defaulton"
|
8
|
+
|
7
9
|
require_relative "http10"
|
8
10
|
require_relative "http11"
|
9
|
-
|
10
11
|
require_relative "http2"
|
11
12
|
|
12
13
|
module Async
|
13
14
|
module HTTP
|
14
15
|
module Protocol
|
15
16
|
# A server that supports both HTTP1.0 and HTTP1.1 semantics by detecting the version of the request.
|
16
|
-
|
17
|
+
class HTTPS
|
18
|
+
# The protocol classes for each supported protocol.
|
17
19
|
HANDLERS = {
|
18
20
|
"h2" => HTTP2,
|
19
21
|
"http/1.1" => HTTP11,
|
@@ -21,11 +23,27 @@ module Async
|
|
21
23
|
nil => HTTP11,
|
22
24
|
}
|
23
25
|
|
24
|
-
def
|
26
|
+
def initialize(handlers = HANDLERS, **options)
|
27
|
+
@handlers = handlers
|
28
|
+
@options = options
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(name, protocol, **options)
|
32
|
+
@handlers[name] = protocol
|
33
|
+
@options[protocol] = options
|
34
|
+
end
|
35
|
+
|
36
|
+
# Determine the protocol of the peer and return the appropriate protocol class.
|
37
|
+
#
|
38
|
+
# Use TLS Application Layer Protocol Negotiation (ALPN) to determine the protocol.
|
39
|
+
#
|
40
|
+
# @parameter peer [IO] The peer to communicate with.
|
41
|
+
# @returns [Class] The protocol class to use.
|
42
|
+
def protocol_for(peer)
|
25
43
|
# alpn_protocol is only available if openssl v1.0.2+
|
26
44
|
name = peer.alpn_protocol
|
27
45
|
|
28
|
-
Console.
|
46
|
+
Console.debug(self) {"Negotiating protocol #{name.inspect}..."}
|
29
47
|
|
30
48
|
if protocol = HANDLERS[name]
|
31
49
|
return protocol
|
@@ -34,18 +52,34 @@ module Async
|
|
34
52
|
end
|
35
53
|
end
|
36
54
|
|
37
|
-
|
38
|
-
|
55
|
+
# Create a client for an outbound connection.
|
56
|
+
#
|
57
|
+
# @parameter peer [IO] The peer to communicate with.
|
58
|
+
# @parameter options [Hash] Options to pass to the client instance.
|
59
|
+
def client(peer, **options)
|
60
|
+
protocol = protocol_for(peer)
|
61
|
+
options = options[protocol] || {}
|
62
|
+
|
63
|
+
protocol.client(peer, **options)
|
39
64
|
end
|
40
65
|
|
41
|
-
|
42
|
-
|
66
|
+
# Create a server for an inbound connection.
|
67
|
+
#
|
68
|
+
# @parameter peer [IO] The peer to communicate with.
|
69
|
+
# @parameter options [Hash] Options to pass to the server instance.
|
70
|
+
def server(peer, **options)
|
71
|
+
protocol = protocol_for(peer)
|
72
|
+
options = options[protocol] || {}
|
73
|
+
|
74
|
+
protocol.server(peer, **options)
|
43
75
|
end
|
44
76
|
|
45
|
-
#
|
46
|
-
def
|
47
|
-
|
77
|
+
# @returns [Array] The names of the supported protocol, used for Application Layer Protocol Negotiation (ALPN).
|
78
|
+
def names
|
79
|
+
@handlers.keys.compact
|
48
80
|
end
|
81
|
+
|
82
|
+
extend Defaulton
|
49
83
|
end
|
50
84
|
end
|
51
85
|
end
|
data/lib/async/http/server.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2017-
|
4
|
+
# Copyright, 2017-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2019, by Brian Morearty.
|
6
6
|
|
7
7
|
require "async"
|
@@ -45,14 +45,14 @@ module Async
|
|
45
45
|
def accept(peer, address, task: Task.current)
|
46
46
|
connection = @protocol.server(peer)
|
47
47
|
|
48
|
-
Console.
|
48
|
+
Console.debug(self) {"Incoming connnection from #{address.inspect} to #{@protocol}"}
|
49
49
|
|
50
50
|
connection.each do |request|
|
51
51
|
# We set the default scheme unless it was otherwise specified.
|
52
52
|
# https://tools.ietf.org/html/rfc7230#section-5.5
|
53
53
|
request.scheme ||= self.scheme
|
54
54
|
|
55
|
-
# Console.
|
55
|
+
# Console.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
|
56
56
|
|
57
57
|
# If this returns nil, we assume that the connection has been hijacked.
|
58
58
|
self.call(request)
|
data/lib/async/http/version.rb
CHANGED
data/license.md
CHANGED
@@ -22,6 +22,7 @@ Copyright, 2023, by dependabot[bot].
|
|
22
22
|
Copyright, 2023, by Josh Huber.
|
23
23
|
Copyright, 2024, by Anton Zhuravsky.
|
24
24
|
Copyright, 2024, by Hal Brodigan.
|
25
|
+
Copyright, 2025, by Jean Boussier.
|
25
26
|
|
26
27
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
27
28
|
of this software and associated documentation files (the "Software"), to deal
|
data/readme.md
CHANGED
@@ -16,6 +16,10 @@ Please see the [project documentation](https://socketry.github.io/async-http/) f
|
|
16
16
|
|
17
17
|
Please see the [project releases](https://socketry.github.io/async-http/releases/index) for all releases.
|
18
18
|
|
19
|
+
### v0.88.0
|
20
|
+
|
21
|
+
- [Support custom protocols with options](https://socketry.github.io/async-http/releases/index#support-custom-protocols-with-options)
|
22
|
+
|
19
23
|
### v0.87.0
|
20
24
|
|
21
25
|
- [Unify HTTP/1 and HTTP/2 `CONNECT` semantics](https://socketry.github.io/async-http/releases/index#unify-http/1-and-http/2-connect-semantics)
|
@@ -55,10 +59,6 @@ Please see the [project releases](https://socketry.github.io/async-http/releases
|
|
55
59
|
|
56
60
|
- [`Async::HTTP::Internet` accepts keyword arguments](https://socketry.github.io/async-http/releases/index#async::http::internet-accepts-keyword-arguments)
|
57
61
|
|
58
|
-
### v0.73.0
|
59
|
-
|
60
|
-
- [Update support for `interim_response`](https://socketry.github.io/async-http/releases/index#update-support-for-interim_response)
|
61
|
-
|
62
62
|
## See Also
|
63
63
|
|
64
64
|
- [benchmark-http](https://github.com/socketry/benchmark-http) — A benchmarking tool to report on web server concurrency.
|
data/releases.md
CHANGED
@@ -1,5 +1,62 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
+
## v0.88.0
|
4
|
+
|
5
|
+
### Support custom protocols with options
|
6
|
+
|
7
|
+
{ruby Async::HTTP::Protocol} contains classes for specific protocols, e.g. {ruby Async::HTTP::Protocol::HTTP1} and {ruby Async::HTTP::Protocol::HTTP2}. It also contains classes for aggregating protocols, e.g. {ruby Async::HTTP::Protocol::HTTP} and {ruby Async::HTTP::Protocol::HTTPS}. They serve as factories for creating client and server instances.
|
8
|
+
|
9
|
+
These classes are now configurable with various options, which are passed as keyword arguments to the relevant connection classes. For example, to configure an HTTP/1.1 protocol without keep-alive:
|
10
|
+
|
11
|
+
``` ruby
|
12
|
+
protocol = Async::HTTP::Protocol::HTTP1.new(persistent: false, maximum_line_length: 32)
|
13
|
+
endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292", protocol: protocol)
|
14
|
+
server = Async::HTTP::Server.for(endpoint) do |request|
|
15
|
+
Protocol::HTTP::Response[200, {}, ["Hello, world"]]
|
16
|
+
end.run
|
17
|
+
```
|
18
|
+
|
19
|
+
Making a request to the server will now close the connection after the response is received:
|
20
|
+
|
21
|
+
> curl -v http://localhost:9292
|
22
|
+
* Host localhost:9292 was resolved.
|
23
|
+
* IPv6: ::1
|
24
|
+
* IPv4: 127.0.0.1
|
25
|
+
* Trying [::1]:9292...
|
26
|
+
* Connected to localhost (::1) port 9292
|
27
|
+
* using HTTP/1.x
|
28
|
+
> GET / HTTP/1.1
|
29
|
+
> Host: localhost:9292
|
30
|
+
> User-Agent: curl/8.12.1
|
31
|
+
> Accept: */*
|
32
|
+
>
|
33
|
+
* Request completely sent off
|
34
|
+
< HTTP/1.1 200 OK
|
35
|
+
< connection: close
|
36
|
+
< content-length: 12
|
37
|
+
<
|
38
|
+
* shutting down connection #0
|
39
|
+
Hello, world
|
40
|
+
|
41
|
+
In addition, any line longer than 32 bytes will be rejected:
|
42
|
+
|
43
|
+
curl -v http://localhost:9292/012345678901234567890123456789012
|
44
|
+
* Host localhost:9292 was resolved.
|
45
|
+
* IPv6: ::1
|
46
|
+
* IPv4: 127.0.0.1
|
47
|
+
* Trying [::1]:9292...
|
48
|
+
* Connected to localhost (::1) port 9292
|
49
|
+
* using HTTP/1.x
|
50
|
+
> GET /012345678901234567890123456789012 HTTP/1.1
|
51
|
+
> Host: localhost:9292
|
52
|
+
> User-Agent: curl/8.12.1
|
53
|
+
> Accept: */*
|
54
|
+
>
|
55
|
+
* Request completely sent off
|
56
|
+
* Empty reply from server
|
57
|
+
* shutting down connection #0
|
58
|
+
curl: (52) Empty reply from server
|
59
|
+
|
3
60
|
## v0.87.0
|
4
61
|
|
5
62
|
### Unify HTTP/1 and HTTP/2 `CONNECT` semantics
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.89.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -16,6 +16,7 @@ authors:
|
|
16
16
|
- Denis Talakevich
|
17
17
|
- Hal Brodigan
|
18
18
|
- Ian Ker-Seymer
|
19
|
+
- Jean Boussier
|
19
20
|
- Josh Huber
|
20
21
|
- Marco Concetto Rudilosso
|
21
22
|
- Olle Jonsson
|
@@ -57,7 +58,7 @@ cert_chain:
|
|
57
58
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
58
59
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
59
60
|
-----END CERTIFICATE-----
|
60
|
-
date: 2025-
|
61
|
+
date: 2025-04-28 00:00:00.000000000 Z
|
61
62
|
dependencies:
|
62
63
|
- !ruby/object:Gem::Dependency
|
63
64
|
name: async
|
@@ -204,6 +205,8 @@ files:
|
|
204
205
|
- lib/async/http/mock.rb
|
205
206
|
- lib/async/http/mock/endpoint.rb
|
206
207
|
- lib/async/http/protocol.rb
|
208
|
+
- lib/async/http/protocol/configurable.rb
|
209
|
+
- lib/async/http/protocol/defaulton.rb
|
207
210
|
- lib/async/http/protocol/http.rb
|
208
211
|
- lib/async/http/protocol/http1.rb
|
209
212
|
- lib/async/http/protocol/http1/client.rb
|
@@ -248,7 +251,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
248
251
|
requirements:
|
249
252
|
- - ">="
|
250
253
|
- !ruby/object:Gem::Version
|
251
|
-
version: '3.
|
254
|
+
version: '3.2'
|
252
255
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
253
256
|
requirements:
|
254
257
|
- - ">="
|
metadata.gz.sig
CHANGED
Binary file
|