async-http 0.87.0 → 0.88.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fb9ca49f85d321261c3a3ee9cccf0459841fc09e5df5981289c6d4a6dc6a973
4
- data.tar.gz: 2a8310e595b65f51001f0b871de5992476153cd0de3eb175d1060e7743ffe6d2
3
+ metadata.gz: 605cb4a26c37dbfdaa3225fb306b3c2bc9e01da814094babb8e46cdb05594999
4
+ data.tar.gz: 40c28864c03492bc20d8aa02947210841ca5f2c75b841e222c90d8f3a36102af
5
5
  SHA512:
6
- metadata.gz: 5b2c843acae23369e6fcffffea81ee443bac6caa2bb21acd80a72af00a831326700e0b0c6e57128f300b11bcbaf435f03e59a84c3de17c59b170241a0dce0cfb
7
- data.tar.gz: b851a56d637736f82cc48b68c00ca4562daa9756548792d4f41c21771150e6b1bd1385eb429cfa43937936454d4719e3535f9273254dc81e26880ce30aa3eeb6
6
+ metadata.gz: 95eb6bb5d079d59b9ed9698d648bd709a7f60ae1a5833547cadd9a11def31e6e8d99b929650df676c0ed215a0719bd604ffc7c57c6e047cdefaa0ac11ff2d71b
7
+ data.tar.gz: 0cdbfe53cbcf3acb1529536ddac851c09e53dd9b7c40fb1d1059abc073138f67cda426a5825966052d52a2d35f39170047c814f7ca6168b25f149fb2baf1e7e0
checksums.yaml.gz.sig CHANGED
Binary file
@@ -30,7 +30,7 @@ def server
30
30
 
31
31
  container = Async::Container.new
32
32
 
33
- Console.logger.info(self){"Starting server..."}
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|
@@ -81,7 +81,7 @@ module Async
81
81
 
82
82
  def close
83
83
  while @pool.busy?
84
- Console.logger.warn(self) {"Waiting for #{@protocol} pool to drain: #{@pool}"}
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.logger.debug(self) {"Making connection to #{@endpoint.inspect}"}
167
+ Console.debug(self) {"Making connection to #{@endpoint.inspect}"}
168
168
 
169
169
  @protocol.client(@endpoint.connect)
170
170
  end
@@ -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 {#initialize}.
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
@@ -4,19 +4,33 @@
4
4
  # Copyright, 2024, by Thomas Morgan.
5
5
  # Copyright, 2024, by Samuel Williams.
6
6
 
7
+ require_relative "defaulton"
8
+
7
9
  require_relative "http1"
8
10
  require_relative "http2"
9
11
 
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
- # connection preface.
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
- def self.protocol_for(stream)
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
- HTTP2
46
+ @http2
33
47
  else
34
- HTTP1
48
+ @http1
35
49
  end
36
50
  end
37
51
 
38
- # Only inbound connections can detect HTTP1 vs HTTP2 for http://.
39
- # Outbound connections default to HTTP1.
40
- def self.client(peer, **options)
41
- HTTP1.client(peer, **options)
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
- def self.server(peer, **options)
45
- stream = ::IO::Stream(peer)
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 protocol_for(stream).server(stream, **options)
71
+ return protocol.server(stream, **options)
48
72
  end
49
73
 
50
- def self.names
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
 
@@ -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
 
@@ -34,7 +34,7 @@ module Async
34
34
  write_body(@version, nil)
35
35
  rescue => error
36
36
  # At this point, there is very little we can do to recover:
37
- Console::Event::Failure.for(error).emit(self, "Failed to write failure response!", severity: :debug)
37
+ Console.debug(self, "Failed to write failure response!", error)
38
38
  end
39
39
 
40
40
  def next_request
@@ -4,6 +4,8 @@
4
4
  # Copyright, 2017-2024, 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
- def self.client(peer)
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
- def self.server(peer)
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
@@ -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
- def self.client(peer)
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
- def self.server(peer)
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
@@ -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
- def self.client(peer)
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
- def self.server(peer)
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
@@ -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)
@@ -26,10 +26,9 @@ module Async
26
26
  TRAILER = "trailer".freeze
27
27
 
28
28
  module Connection
29
- def initialize(*)
29
+ def initialize(...)
30
30
  super
31
31
 
32
- @count = 0
33
32
  @reader = nil
34
33
 
35
34
  # 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 +40,7 @@ module Async
41
40
  end
42
41
 
43
42
  def to_s
44
- "\#<#{self.class} #{@count} requests, #{@streams.count} active streams>"
43
+ "\#<#{self.class} #{@streams.count} active streams>"
45
44
  end
46
45
 
47
46
  def as_json(...)
@@ -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
@@ -65,7 +65,7 @@ module Async
65
65
  @input.close_write
66
66
  end
67
67
  rescue ::Protocol::HTTP2::HeaderError => error
68
- Console.logger.debug(self, error)
68
+ Console.debug(self, "Error while processing headers!", error)
69
69
 
70
70
  send_reset_stream(error.code)
71
71
  end
@@ -4,6 +4,8 @@
4
4
  # Copyright, 2018-2024, 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
- def self.client(peer, settings = CLIENT_SETTINGS)
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
- def self.server(peer, settings = SERVER_SETTINGS)
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
@@ -4,16 +4,18 @@
4
4
  # Copyright, 2018-2024, 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
- module HTTPS
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 self.protocol_for(peer)
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.logger.debug(self) {"Negotiating protocol #{name.inspect}..."}
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
- def self.client(peer)
38
- protocol_for(peer).client(peer)
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
- def self.server(peer)
42
- protocol_for(peer).server(peer)
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
- # Supported Application Layer Protocol Negotiation names:
46
- def self.names
47
- HANDLERS.keys.compact
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
@@ -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.logger.debug(self) {"Incoming connnection from #{address.inspect} to #{@protocol}"}
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.logger.debug(self) {"Incoming request from #{address.inspect}: #{request.method} #{request.path}"}
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)
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module HTTP
8
- VERSION = "0.87.0"
8
+ VERSION = "0.88.0"
9
9
  end
10
10
  end
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.87.0
4
+ version: 0.88.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -57,7 +57,7 @@ cert_chain:
57
57
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
58
58
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
59
59
  -----END CERTIFICATE-----
60
- date: 2025-01-29 00:00:00.000000000 Z
60
+ date: 2025-03-13 00:00:00.000000000 Z
61
61
  dependencies:
62
62
  - !ruby/object:Gem::Dependency
63
63
  name: async
@@ -204,6 +204,8 @@ files:
204
204
  - lib/async/http/mock.rb
205
205
  - lib/async/http/mock/endpoint.rb
206
206
  - lib/async/http/protocol.rb
207
+ - lib/async/http/protocol/configurable.rb
208
+ - lib/async/http/protocol/defaulton.rb
207
209
  - lib/async/http/protocol/http.rb
208
210
  - lib/async/http/protocol/http1.rb
209
211
  - lib/async/http/protocol/http1/client.rb
metadata.gz.sig CHANGED
Binary file