protocol-rack 0.12.0 → 0.13.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 +0 -0
- data/lib/protocol/rack/adapter/generic.rb +34 -5
- data/lib/protocol/rack/adapter/rack2.rb +46 -10
- data/lib/protocol/rack/adapter/rack3.rb +35 -6
- data/lib/protocol/rack/adapter/rack31.rb +16 -4
- data/lib/protocol/rack/adapter.rb +23 -1
- data/lib/protocol/rack/body/enumerable.rb +52 -17
- data/lib/protocol/rack/body/input_wrapper.rb +17 -8
- data/lib/protocol/rack/body.rb +30 -1
- data/lib/protocol/rack/input.rb +11 -5
- data/lib/protocol/rack/request.rb +22 -1
- data/lib/protocol/rack/response.rb +1 -1
- data/lib/protocol/rack/rewindable.rb +22 -5
- data/lib/protocol/rack/version.rb +1 -1
- data/lib/protocol/rack.rb +8 -1
- data/readme.md +10 -0
- data/releases.md +10 -0
- data.tar.gz.sig +0 -0
- metadata +2 -2
- 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: b70faf13a5271971f02ddae3e19e17262c9df2ce74832cce7e0f6312265d76df
|
4
|
+
data.tar.gz: e9a4a1dfc0cb419d6395a7e68300f270e26398fadb00661b08093c60b47b7ea2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d8a7f11acb1220d0ed05626c4ddfb87ef23578922741e92d15bf32e0e0294c433ebfa2149e538f10352695b3c0cf30af69667ab34033beead13f799dbbbdad5
|
7
|
+
data.tar.gz: 4d59623d81149838f7b2bf5f0ce67a13bdf39a0d5b04e17367ab6d07d57468d008834d6ad439cd6f41e42dcf95e6d314694334b38bfcfe02ba0d96f5a35b5def
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
@@ -1,35 +1,52 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "console"
|
7
7
|
|
8
8
|
require_relative "../constants"
|
9
9
|
require_relative "../input"
|
10
10
|
require_relative "../response"
|
11
|
+
require_relative "../rewindable"
|
11
12
|
|
12
13
|
module Protocol
|
13
14
|
module Rack
|
14
15
|
module Adapter
|
16
|
+
# The base adapter class that provides common functionality for all Rack adapters.
|
17
|
+
# It handles the conversion between {Protocol::HTTP} and Rack environments.
|
15
18
|
class Generic
|
19
|
+
# Creates a new adapter instance for the given Rack application.
|
20
|
+
# Wraps the adapter in a {Rewindable} instance to ensure request body can be read multiple times, which is required for Rack < 3.
|
21
|
+
#
|
22
|
+
# @parameter app [Interface(:call)] A Rack application.
|
23
|
+
# @returns [Rewindable] A rewindable adapter instance.
|
16
24
|
def self.wrap(app)
|
17
|
-
self.new(app)
|
25
|
+
Rewindable.new(self.new(app))
|
18
26
|
end
|
19
27
|
|
28
|
+
# Parses a Rackup file and returns the application.
|
29
|
+
#
|
30
|
+
# @parameter path [String] The path to the Rackup file.
|
31
|
+
# @returns [Interface(:call)] The Rack application.
|
20
32
|
def self.parse_file(...)
|
21
33
|
# This is the old interface, which was changed in Rack 3.
|
22
34
|
::Rack::Builder.parse_file(...).first
|
23
35
|
end
|
24
36
|
|
25
37
|
# Initialize the rack adaptor middleware.
|
26
|
-
#
|
38
|
+
#
|
39
|
+
# @parameter app [Interface(:call)] The rack middleware.
|
40
|
+
# @raises [ArgumentError] If the app does not respond to `call`.
|
27
41
|
def initialize(app)
|
28
42
|
@app = app
|
29
43
|
|
30
44
|
raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call)
|
31
45
|
end
|
32
46
|
|
47
|
+
# The logger to use for this adapter.
|
48
|
+
#
|
49
|
+
# @returns [Console] The console logger.
|
33
50
|
def logger
|
34
51
|
Console
|
35
52
|
end
|
@@ -108,6 +125,10 @@ module Protocol
|
|
108
125
|
end
|
109
126
|
end
|
110
127
|
|
128
|
+
# Create a base environment hash for the request.
|
129
|
+
#
|
130
|
+
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
131
|
+
# @returns [Hash] The base environment hash.
|
111
132
|
def make_environment(request)
|
112
133
|
{
|
113
134
|
request: request
|
@@ -117,6 +138,8 @@ module Protocol
|
|
117
138
|
# Build a rack `env` from the incoming request and apply it to the rack middleware.
|
118
139
|
#
|
119
140
|
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
141
|
+
# @returns [Protocol::HTTP::Response] The HTTP response.
|
142
|
+
# @raises [ArgumentError] If the status is not an integer or headers are nil.
|
120
143
|
def call(request)
|
121
144
|
env = self.make_environment(request)
|
122
145
|
|
@@ -148,12 +171,18 @@ module Protocol
|
|
148
171
|
end
|
149
172
|
|
150
173
|
# Generate a suitable response for the given exception.
|
151
|
-
#
|
152
|
-
# @
|
174
|
+
#
|
175
|
+
# @parameter exception [Exception] The exception that occurred.
|
176
|
+
# @returns [Protocol::HTTP::Response] A response representing the error.
|
153
177
|
def failure_response(exception)
|
154
178
|
Protocol::HTTP::Response.for_exception(exception)
|
155
179
|
end
|
156
180
|
|
181
|
+
# Extract protocol information from the environment and response.
|
182
|
+
#
|
183
|
+
# @parameter env [Hash] The rack environment.
|
184
|
+
# @parameter response [Protocol::HTTP::Response] The HTTP response.
|
185
|
+
# @parameter headers [Hash] The response headers to modify.
|
157
186
|
def self.extract_protocol(env, response, headers)
|
158
187
|
if protocol = response.protocol
|
159
188
|
# This is the newer mechanism for protocol upgrade:
|
@@ -12,16 +12,23 @@ require_relative "../rewindable"
|
|
12
12
|
module Protocol
|
13
13
|
module Rack
|
14
14
|
module Adapter
|
15
|
+
# The Rack 2 adapter provides compatibility with Rack 2.x applications.
|
16
|
+
# It handles the conversion between {Protocol::HTTP} and Rack 2 environments.
|
15
17
|
class Rack2 < Generic
|
18
|
+
# The Rack version constant.
|
16
19
|
RACK_VERSION = "rack.version"
|
20
|
+
# Whether the application is multithreaded.
|
17
21
|
RACK_MULTITHREAD = "rack.multithread"
|
22
|
+
# Whether the application is multiprocess.
|
18
23
|
RACK_MULTIPROCESS = "rack.multiprocess"
|
24
|
+
# Whether the application should run only once.
|
19
25
|
RACK_RUN_ONCE = "rack.run_once"
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
27
|
+
# Create a Rack 2 environment hash for the request.
|
28
|
+
# Sets up all required Rack 2 environment variables and processes the request.
|
29
|
+
#
|
30
|
+
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
31
|
+
# @returns [Hash] The Rack 2 environment hash.
|
25
32
|
def make_environment(request)
|
26
33
|
request_path, query_string = request.path.split("?", 2)
|
27
34
|
server_name, server_port = (request.authority || "").split(":", 2)
|
@@ -38,13 +45,13 @@ module Protocol
|
|
38
45
|
RACK_ERRORS => $stderr,
|
39
46
|
RACK_LOGGER => self.logger,
|
40
47
|
|
41
|
-
# The HTTP request method, such as
|
48
|
+
# The HTTP request method, such as "GET" or "POST". This cannot ever be an empty string, and so is always required.
|
42
49
|
CGI::REQUEST_METHOD => request.method,
|
43
50
|
|
44
|
-
# The initial portion of the request URL's
|
51
|
+
# The initial portion of the request URL's "path" that corresponds to the application object, so that the application knows its virtual "location". This may be an empty string, if the application corresponds to the "root" of the server.
|
45
52
|
CGI::SCRIPT_NAME => "",
|
46
53
|
|
47
|
-
# The remainder of the request URL's
|
54
|
+
# The remainder of the request URL's "path", designating the virtual "location" of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
|
48
55
|
CGI::PATH_INFO => request_path,
|
49
56
|
CGI::REQUEST_PATH => request_path,
|
50
57
|
CGI::REQUEST_URI => request.path,
|
@@ -75,11 +82,27 @@ module Protocol
|
|
75
82
|
# Build a rack `env` from the incoming request and apply it to the rack middleware.
|
76
83
|
#
|
77
84
|
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
85
|
+
# @returns [Protocol::HTTP::Response] The HTTP response.
|
86
|
+
# @raises [ArgumentError] If the status is not an integer or headers are nil.
|
78
87
|
def call(request)
|
79
88
|
env = self.make_environment(request)
|
80
89
|
|
81
90
|
status, headers, body = @app.call(env)
|
82
91
|
|
92
|
+
if status
|
93
|
+
status = status.to_i
|
94
|
+
else
|
95
|
+
raise ArgumentError, "Status must be an integer!"
|
96
|
+
end
|
97
|
+
|
98
|
+
unless headers
|
99
|
+
raise ArgumentError, "Headers must not be nil!"
|
100
|
+
end
|
101
|
+
|
102
|
+
# unless body.respond_to?(:each)
|
103
|
+
# raise ArgumentError, "Body must respond to #each!"
|
104
|
+
# end
|
105
|
+
|
83
106
|
headers, meta = self.wrap_headers(headers)
|
84
107
|
|
85
108
|
# Rack 2 spec does not allow only partial hijacking.
|
@@ -96,8 +119,11 @@ module Protocol
|
|
96
119
|
return failure_response(exception)
|
97
120
|
end
|
98
121
|
|
99
|
-
# Process the rack response headers into
|
100
|
-
#
|
122
|
+
# Process the rack response headers into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
|
123
|
+
# Headers with newline-separated values are split into multiple headers.
|
124
|
+
#
|
125
|
+
# @parameter fields [Hash] The raw response headers.
|
126
|
+
# @returns [Tuple(Protocol::HTTP::Headers, Hash)] The processed headers and metadata.
|
101
127
|
def wrap_headers(fields)
|
102
128
|
headers = ::Protocol::HTTP::Headers.new
|
103
129
|
meta = {}
|
@@ -119,6 +145,12 @@ module Protocol
|
|
119
145
|
return headers, meta
|
120
146
|
end
|
121
147
|
|
148
|
+
# Convert a {Protocol::HTTP::Response} into a Rack 2 response tuple.
|
149
|
+
# Handles protocol upgrades and streaming responses.
|
150
|
+
#
|
151
|
+
# @parameter env [Hash] The rack environment.
|
152
|
+
# @parameter response [Protocol::HTTP::Response] The HTTP response.
|
153
|
+
# @returns [Tuple(Integer, Hash, Object)] The Rack 2 response tuple [status, headers, body].
|
122
154
|
def self.make_response(env, response)
|
123
155
|
# These interfaces should be largely compatible:
|
124
156
|
headers = response.headers.to_h
|
@@ -133,7 +165,11 @@ module Protocol
|
|
133
165
|
end
|
134
166
|
|
135
167
|
headers.transform_values! do |value|
|
136
|
-
value.is_a?(Array)
|
168
|
+
if value.is_a?(Array)
|
169
|
+
value.join("\n")
|
170
|
+
else
|
171
|
+
value
|
172
|
+
end
|
137
173
|
end
|
138
174
|
|
139
175
|
[response.status, headers, body]
|
@@ -11,15 +11,34 @@ require_relative "generic"
|
|
11
11
|
module Protocol
|
12
12
|
module Rack
|
13
13
|
module Adapter
|
14
|
+
# The Rack 3 adapter provides compatibility with Rack 3.x applications.
|
15
|
+
# It handles the conversion between {Protocol::HTTP} and Rack 3 environments.
|
16
|
+
# Unlike Rack 2, this adapter supports streaming responses and has a simpler environment setup.
|
14
17
|
class Rack3 < Generic
|
18
|
+
# Creates a new adapter instance for the given Rack application.
|
19
|
+
# Unlike Rack 2, this adapter doesn't require a {Rewindable} wrapper.
|
20
|
+
#
|
21
|
+
# @parameter app [Interface(:call)] A Rack application.
|
22
|
+
# @returns [Rack3] A new adapter instance.
|
15
23
|
def self.wrap(app)
|
16
24
|
self.new(app)
|
17
25
|
end
|
18
26
|
|
27
|
+
# Parses a Rackup file and returns the application.
|
28
|
+
# Uses the Rack 3.x interface for parsing Rackup files.
|
29
|
+
#
|
30
|
+
# @parameter path [String] The path to the Rackup file.
|
31
|
+
# @returns [Interface(:call)] The Rack application.
|
19
32
|
def self.parse_file(...)
|
20
33
|
::Rack::Builder.parse_file(...)
|
21
34
|
end
|
22
35
|
|
36
|
+
# Create a Rack 3 environment hash for the request.
|
37
|
+
# Sets up all required Rack 3 environment variables and processes the request.
|
38
|
+
# Unlike Rack 2, this adapter doesn't set Rack version or threading flags.
|
39
|
+
#
|
40
|
+
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
41
|
+
# @returns [Hash] The Rack 3 environment hash.
|
23
42
|
def make_environment(request)
|
24
43
|
request_path, query_string = request.path.split("?", 2)
|
25
44
|
server_name, server_port = (request.authority || "").split(":", 2)
|
@@ -34,13 +53,13 @@ module Protocol
|
|
34
53
|
# The response finished callbacks:
|
35
54
|
RACK_RESPONSE_FINISHED => [],
|
36
55
|
|
37
|
-
# The HTTP request method, such as
|
56
|
+
# The HTTP request method, such as "GET" or "POST". This cannot ever be an empty string, and so is always required.
|
38
57
|
CGI::REQUEST_METHOD => request.method,
|
39
58
|
|
40
|
-
# The initial portion of the request URL's
|
59
|
+
# The initial portion of the request URL's "path" that corresponds to the application object, so that the application knows its virtual "location". This may be an empty string, if the application corresponds to the "root" of the server.
|
41
60
|
CGI::SCRIPT_NAME => "",
|
42
61
|
|
43
|
-
# The remainder of the request URL's
|
62
|
+
# The remainder of the request URL's "path", designating the virtual "location" of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
|
44
63
|
CGI::PATH_INFO => request_path,
|
45
64
|
CGI::REQUEST_PATH => request_path,
|
46
65
|
CGI::REQUEST_URI => request.path,
|
@@ -57,7 +76,7 @@ module Protocol
|
|
57
76
|
# I'm not sure what sane defaults should be here:
|
58
77
|
CGI::SERVER_NAME => server_name,
|
59
78
|
}
|
60
|
-
|
79
|
+
|
61
80
|
# SERVER_PORT is optional but must not be set if it is not present.
|
62
81
|
if server_port
|
63
82
|
env[CGI::SERVER_PORT] = server_port
|
@@ -68,8 +87,11 @@ module Protocol
|
|
68
87
|
return env
|
69
88
|
end
|
70
89
|
|
71
|
-
# Process the rack response headers into
|
72
|
-
#
|
90
|
+
# Process the rack response headers into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
|
91
|
+
# Unlike Rack 2, this adapter handles array values directly without splitting on newlines.
|
92
|
+
#
|
93
|
+
# @parameter fields [Hash] The raw response headers.
|
94
|
+
# @returns [Tuple(Protocol::HTTP::Headers, Hash)] The processed headers and metadata.
|
73
95
|
def wrap_headers(fields)
|
74
96
|
headers = ::Protocol::HTTP::Headers.new
|
75
97
|
meta = {}
|
@@ -91,6 +113,13 @@ module Protocol
|
|
91
113
|
return headers, meta
|
92
114
|
end
|
93
115
|
|
116
|
+
# Convert a {Protocol::HTTP::Response} into a Rack 3 response tuple.
|
117
|
+
# Handles protocol upgrades and streaming responses.
|
118
|
+
# Unlike Rack 2, this adapter forces streaming responses by converting the body to a callable.
|
119
|
+
#
|
120
|
+
# @parameter env [Hash] The rack environment.
|
121
|
+
# @parameter response [Protocol::HTTP::Response] The HTTP response.
|
122
|
+
# @returns [Tuple(Integer, Hash, Object)] The Rack 3 response tuple [status, headers, body].
|
94
123
|
def self.make_response(env, response)
|
95
124
|
# These interfaces should be largely compatible:
|
96
125
|
headers = response.headers.to_h
|
@@ -11,7 +11,19 @@ require_relative "rack3"
|
|
11
11
|
module Protocol
|
12
12
|
module Rack
|
13
13
|
module Adapter
|
14
|
+
# The Rack 3.1 adapter provides compatibility with Rack 3.1.x applications.
|
15
|
+
# It extends the Rack 3 adapter with improved request body handling and protocol support.
|
16
|
+
# Key improvements include:
|
17
|
+
# - Better handling of empty request bodies
|
18
|
+
# - Direct protocol support via {RACK_PROTOCOL}
|
19
|
+
# - More efficient body streaming
|
14
20
|
class Rack31 < Rack3
|
21
|
+
# Create a Rack 3.1 environment hash for the request.
|
22
|
+
# Sets up all required Rack 3.1 environment variables and processes the request.
|
23
|
+
# Unlike Rack 3, this adapter has improved body handling and protocol support.
|
24
|
+
#
|
25
|
+
# @parameter request [Protocol::HTTP::Request] The incoming request.
|
26
|
+
# @returns [Hash] The Rack 3.1 environment hash.
|
15
27
|
def make_environment(request)
|
16
28
|
request_path, query_string = request.path.split("?", 2)
|
17
29
|
server_name, server_port = (request.authority || "").split(":", 2)
|
@@ -25,13 +37,13 @@ module Protocol
|
|
25
37
|
# The response finished callbacks:
|
26
38
|
RACK_RESPONSE_FINISHED => [],
|
27
39
|
|
28
|
-
# The HTTP request method, such as
|
40
|
+
# The HTTP request method, such as "GET" or "POST". This cannot ever be an empty string, and so is always required.
|
29
41
|
CGI::REQUEST_METHOD => request.method,
|
30
42
|
|
31
|
-
# The initial portion of the request URL's
|
43
|
+
# The initial portion of the request URL's "path" that corresponds to the application object, so that the application knows its virtual "location". This may be an empty string, if the application corresponds to the "root" of the server.
|
32
44
|
CGI::SCRIPT_NAME => "",
|
33
45
|
|
34
|
-
# The remainder of the request URL's
|
46
|
+
# The remainder of the request URL's "path", designating the virtual "location" of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
|
35
47
|
CGI::PATH_INFO => request_path,
|
36
48
|
CGI::REQUEST_PATH => request_path,
|
37
49
|
CGI::REQUEST_URI => request.path,
|
@@ -48,7 +60,7 @@ module Protocol
|
|
48
60
|
# I'm not sure what sane defaults should be here:
|
49
61
|
CGI::SERVER_NAME => server_name,
|
50
62
|
}
|
51
|
-
|
63
|
+
|
52
64
|
# SERVER_PORT is optional but must not be set if it is not present.
|
53
65
|
if server_port
|
54
66
|
env[CGI::SERVER_PORT] = server_port
|
@@ -1,13 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "rack"
|
7
7
|
|
8
8
|
module Protocol
|
9
9
|
module Rack
|
10
|
+
# The Rack adapter provides a bridge between Protocol::HTTP and Rack applications.
|
11
|
+
# It automatically selects the appropriate implementation based on the installed Rack version.
|
12
|
+
#
|
13
|
+
# ```ruby
|
14
|
+
# app = ->(env) { [200, {"content-type" => "text/plain"}, ["Hello World"]] }
|
15
|
+
# adapter = Protocol::Rack::Adapter.new(app)
|
16
|
+
# response = adapter.call(request)
|
17
|
+
# ```
|
10
18
|
module Adapter
|
19
|
+
# The version of Rack being used. Can be overridden using the PROTOCOL_RACK_ADAPTER_VERSION environment variable.
|
11
20
|
VERSION = ENV.fetch("PROTOCOL_RACK_ADAPTER_VERSION", ::Rack.release)
|
12
21
|
|
13
22
|
if VERSION >= "3.1"
|
@@ -21,14 +30,27 @@ module Protocol
|
|
21
30
|
IMPLEMENTATION = Rack2
|
22
31
|
end
|
23
32
|
|
33
|
+
# Creates a new adapter instance for the given Rack application.
|
34
|
+
#
|
35
|
+
# @parameter app [Interface(:call)] A Rack application that responds to #call
|
36
|
+
# @returns [Protocol::HTTP::Middleware] An adapter that can handle HTTP requests
|
24
37
|
def self.new(app)
|
25
38
|
IMPLEMENTATION.wrap(app)
|
26
39
|
end
|
27
40
|
|
41
|
+
# Converts a Rack response into a Protocol::HTTP response.
|
42
|
+
#
|
43
|
+
# @parameter env [Hash] The Rack environment
|
44
|
+
# @parameter response [Array] The Rack response [status, headers, body]
|
45
|
+
# @returns [Protocol::HTTP::Response] A Protocol::HTTP response
|
28
46
|
def self.make_response(env, response)
|
29
47
|
IMPLEMENTATION.make_response(env, response)
|
30
48
|
end
|
31
49
|
|
50
|
+
# Parses a file path from the Rack environment.
|
51
|
+
#
|
52
|
+
# @parameter env [Hash] The Rack environment
|
53
|
+
# @returns [String | Nil] The parsed file path or nil if not found
|
32
54
|
def self.parse_file(...)
|
33
55
|
IMPLEMENTATION.parse_file(...)
|
34
56
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "protocol/http/body/readable"
|
7
7
|
require "protocol/http/body/file"
|
@@ -9,14 +9,19 @@ require "protocol/http/body/file"
|
|
9
9
|
module Protocol
|
10
10
|
module Rack
|
11
11
|
module Body
|
12
|
-
# Wraps
|
13
|
-
#
|
14
|
-
#
|
12
|
+
# Wraps a Rack response body that responds to `each`.
|
13
|
+
# The body must only yield `String` values and may optionally respond to `close`.
|
14
|
+
# This class provides both streaming and buffered access to the response body.
|
15
15
|
class Enumerable < ::Protocol::HTTP::Body::Readable
|
16
|
+
# The content-length header key.
|
16
17
|
CONTENT_LENGTH = "content-length".freeze
|
17
18
|
|
18
|
-
# Wraps
|
19
|
-
#
|
19
|
+
# Wraps a Rack response body into an {Enumerable} instance.
|
20
|
+
# If the body is an Array, its total size is calculated automatically.
|
21
|
+
#
|
22
|
+
# @parameter body [Object] The Rack response body that responds to `each`.
|
23
|
+
# @parameter length [Integer] Optional content length of the response body.
|
24
|
+
# @returns [Enumerable] A new enumerable body instance.
|
20
25
|
def self.wrap(body, length = nil)
|
21
26
|
if body.is_a?(Array)
|
22
27
|
length ||= body.sum(&:bytesize)
|
@@ -26,9 +31,10 @@ module Protocol
|
|
26
31
|
end
|
27
32
|
end
|
28
33
|
|
29
|
-
# Initialize the
|
30
|
-
#
|
31
|
-
# @parameter
|
34
|
+
# Initialize the enumerable body wrapper.
|
35
|
+
#
|
36
|
+
# @parameter body [Object] The Rack response body that responds to `each`.
|
37
|
+
# @parameter length [Integer] The content length of the response body.
|
32
38
|
def initialize(body, length)
|
33
39
|
@length = length
|
34
40
|
@body = body
|
@@ -36,23 +42,32 @@ module Protocol
|
|
36
42
|
@chunks = nil
|
37
43
|
end
|
38
44
|
|
39
|
-
# The
|
45
|
+
# @attribute [Object] The wrapped Rack response body.
|
40
46
|
attr :body
|
41
47
|
|
42
|
-
# The
|
48
|
+
# @attribute [Integer] The total size of the response body in bytes.
|
43
49
|
attr :length
|
44
50
|
|
45
|
-
#
|
51
|
+
# Check if the response body is empty.
|
52
|
+
# A body is considered empty if its length is 0 or if it responds to `empty?` and is empty.
|
53
|
+
#
|
54
|
+
# @returns [Boolean] True if the body is empty.
|
46
55
|
def empty?
|
47
56
|
@length == 0 or (@body.respond_to?(:empty?) and @body.empty?)
|
48
57
|
end
|
49
58
|
|
50
|
-
#
|
59
|
+
# Check if the response body can be read immediately.
|
60
|
+
# A body is ready if it's an Array or responds to `to_ary`.
|
61
|
+
#
|
62
|
+
# @returns [Boolean] True if the body can be read immediately.
|
51
63
|
def ready?
|
52
64
|
body.is_a?(Array) or body.respond_to?(:to_ary)
|
53
65
|
end
|
54
66
|
|
55
67
|
# Close the response body.
|
68
|
+
# If the body responds to `close`, it will be called.
|
69
|
+
#
|
70
|
+
# @parameter error [Exception] Optional error that occurred during processing.
|
56
71
|
def close(error = nil)
|
57
72
|
if @body and @body.respond_to?(:close)
|
58
73
|
@body.close
|
@@ -65,26 +80,43 @@ module Protocol
|
|
65
80
|
end
|
66
81
|
|
67
82
|
# Enumerate the response body.
|
83
|
+
# Each chunk yielded must be a String.
|
84
|
+
# The body is automatically closed after enumeration.
|
85
|
+
#
|
68
86
|
# @yields {|chunk| ...}
|
69
|
-
# @parameter chunk [String]
|
87
|
+
# @parameter chunk [String] A chunk of the response body.
|
70
88
|
def each(&block)
|
71
89
|
@body.each(&block)
|
90
|
+
rescue => error
|
91
|
+
raise
|
72
92
|
ensure
|
73
|
-
self.close(
|
93
|
+
self.close(error)
|
74
94
|
end
|
75
95
|
|
96
|
+
# Check if the body is a streaming response.
|
97
|
+
# A body is streaming if it doesn't respond to `each`.
|
98
|
+
#
|
99
|
+
# @returns [Boolean] True if the body is streaming.
|
76
100
|
def stream?
|
77
101
|
!@body.respond_to?(:each)
|
78
102
|
end
|
79
103
|
|
104
|
+
# Stream the response body to the given stream.
|
105
|
+
# The body is automatically closed after streaming.
|
106
|
+
#
|
107
|
+
# @parameter stream [Object] The stream to write the body to.
|
80
108
|
def call(stream)
|
81
109
|
@body.call(stream)
|
110
|
+
rescue => error
|
111
|
+
raise
|
82
112
|
ensure
|
83
|
-
self.close(
|
113
|
+
self.close(error)
|
84
114
|
end
|
85
115
|
|
86
116
|
# Read the next chunk from the response body.
|
87
|
-
#
|
117
|
+
# Returns nil when there are no more chunks.
|
118
|
+
#
|
119
|
+
# @returns [String | Nil] The next chunk or nil if there are no more chunks.
|
88
120
|
def read
|
89
121
|
@chunks ||= @body.to_enum(:each)
|
90
122
|
|
@@ -93,6 +125,9 @@ module Protocol
|
|
93
125
|
return nil
|
94
126
|
end
|
95
127
|
|
128
|
+
# Get a string representation of the body.
|
129
|
+
#
|
130
|
+
# @returns [String] A string describing the body's class and length.
|
96
131
|
def inspect
|
97
132
|
"\#<#{self.class} length=#{@length.inspect} body=#{@body.class}>"
|
98
133
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "protocol/http/body/readable"
|
7
7
|
require "protocol/http/body/stream"
|
@@ -9,10 +9,17 @@ require "protocol/http/body/stream"
|
|
9
9
|
module Protocol
|
10
10
|
module Rack
|
11
11
|
module Body
|
12
|
-
#
|
12
|
+
# Wraps a Rack input object into a readable body.
|
13
|
+
# This class provides a consistent interface for reading from Rack input streams,
|
14
|
+
# which may be any IO-like object that responds to `read` and `close`.
|
13
15
|
class InputWrapper < Protocol::HTTP::Body::Readable
|
16
|
+
# The default block size for reading from the input stream.
|
14
17
|
BLOCK_SIZE = 1024*4
|
15
18
|
|
19
|
+
# Initialize the input wrapper.
|
20
|
+
#
|
21
|
+
# @parameter io [Object] The input object that responds to `read` and `close`.
|
22
|
+
# @parameter block_size [Integer] The size of chunks to read at a time.
|
16
23
|
def initialize(io, block_size: BLOCK_SIZE)
|
17
24
|
@io = io
|
18
25
|
@block_size = block_size
|
@@ -20,6 +27,10 @@ module Protocol
|
|
20
27
|
super()
|
21
28
|
end
|
22
29
|
|
30
|
+
# Close the input stream.
|
31
|
+
# If the input object responds to `close`, it will be called.
|
32
|
+
#
|
33
|
+
# @parameter error [Exception] Optional error that occurred during processing.
|
23
34
|
def close(error = nil)
|
24
35
|
if @io
|
25
36
|
@io.close
|
@@ -27,12 +38,10 @@ module Protocol
|
|
27
38
|
end
|
28
39
|
end
|
29
40
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
# end
|
35
|
-
|
41
|
+
# Read the next chunk from the input stream.
|
42
|
+
# Returns nil when there is no more data to read.
|
43
|
+
#
|
44
|
+
# @returns [String | Nil] The next chunk of data or nil if there is no more data.
|
36
45
|
def read
|
37
46
|
@io&.read(@block_size)
|
38
47
|
end
|
data/lib/protocol/rack/body.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "body/streaming"
|
7
7
|
require_relative "body/enumerable"
|
@@ -10,13 +10,34 @@ require "protocol/http/body/completable"
|
|
10
10
|
|
11
11
|
module Protocol
|
12
12
|
module Rack
|
13
|
+
# The Body module provides functionality for handling Rack response bodies.
|
14
|
+
# It includes methods for wrapping different types of response bodies and handling completion callbacks.
|
13
15
|
module Body
|
16
|
+
# The `content-length` header key.
|
14
17
|
CONTENT_LENGTH = "content-length"
|
15
18
|
|
19
|
+
# Check if the given status code indicates no content should be returned.
|
20
|
+
# Status codes 204 (No Content), 205 (Reset Content), and 304 (Not Modified) should not include a response body.
|
21
|
+
#
|
22
|
+
# @parameter status [Integer] The HTTP status code.
|
23
|
+
# @returns [Boolean] True if the status code indicates no content.
|
16
24
|
def self.no_content?(status)
|
17
25
|
status == 204 or status == 205 or status == 304
|
18
26
|
end
|
19
27
|
|
28
|
+
# Wrap a Rack response body into a {Protocol::HTTP::Body} instance.
|
29
|
+
# Handles different types of response bodies:
|
30
|
+
# - {Protocol::HTTP::Body::Readable} instances are returned as-is.
|
31
|
+
# - Bodies that respond to `to_path` are wrapped in {Protocol::HTTP::Body::File}.
|
32
|
+
# - Enumerable bodies are wrapped in {Body::Enumerable}.
|
33
|
+
# - Other bodies are wrapped in {Body::Streaming}.
|
34
|
+
#
|
35
|
+
# @parameter env [Hash] The Rack environment.
|
36
|
+
# @parameter status [Integer] The HTTP status code.
|
37
|
+
# @parameter headers [Hash] The response headers.
|
38
|
+
# @parameter body [Object] The response body to wrap.
|
39
|
+
# @parameter input [Object] Optional input for streaming bodies.
|
40
|
+
# @returns [Protocol::HTTP::Body] The wrapped response body.
|
20
41
|
def self.wrap(env, status, headers, body, input = nil)
|
21
42
|
# In no circumstance do we want this header propagating out:
|
22
43
|
if length = headers.delete(CONTENT_LENGTH)
|
@@ -66,6 +87,14 @@ module Protocol
|
|
66
87
|
return body
|
67
88
|
end
|
68
89
|
|
90
|
+
# Create a completion callback for response finished handlers.
|
91
|
+
# The callback is called with any error that occurred during response processing.
|
92
|
+
#
|
93
|
+
# @parameter response_finished [Array] Array of response finished callbacks.
|
94
|
+
# @parameter env [Hash] The Rack environment.
|
95
|
+
# @parameter status [Integer] The HTTP status code.
|
96
|
+
# @parameter headers [Hash] The response headers.
|
97
|
+
# @returns [Proc] A callback that calls all response finished handlers.
|
69
98
|
def self.completion_callback(response_finished, env, status, headers)
|
70
99
|
proc do |error|
|
71
100
|
response_finished.each do |callback|
|
data/lib/protocol/rack/input.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
# Copyright, 2023, by Genki Takiuchi.
|
6
6
|
|
7
7
|
require "protocol/http/body/stream"
|
@@ -64,8 +64,10 @@ module Protocol
|
|
64
64
|
if @body and @body.respond_to?(:rewind)
|
65
65
|
# If the body is not rewindable, this will fail.
|
66
66
|
@body.rewind
|
67
|
+
|
67
68
|
@buffer = nil
|
68
69
|
@finished = false
|
70
|
+
@closed = false
|
69
71
|
|
70
72
|
return true
|
71
73
|
end
|
@@ -93,14 +95,18 @@ module Protocol
|
|
93
95
|
# https://github.com/socketry/async-http/issues/183
|
94
96
|
if @body.empty?
|
95
97
|
@body.close
|
96
|
-
@
|
98
|
+
@closed = true
|
97
99
|
end
|
98
100
|
|
99
101
|
return chunk
|
100
102
|
else
|
101
|
-
|
102
|
-
|
103
|
-
|
103
|
+
unless @closed
|
104
|
+
# So if we are at the end of the stream, we close it automatically:
|
105
|
+
@body.close
|
106
|
+
@closed = true
|
107
|
+
end
|
108
|
+
|
109
|
+
return nil
|
104
110
|
end
|
105
111
|
elsif @closed
|
106
112
|
raise IOError, "Stream is not readable, input has been closed!"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "protocol/http/request"
|
7
7
|
require "protocol/http/headers"
|
@@ -11,11 +11,22 @@ require_relative "body/input_wrapper"
|
|
11
11
|
|
12
12
|
module Protocol
|
13
13
|
module Rack
|
14
|
+
# A Rack-compatible HTTP request wrapper.
|
15
|
+
# This class provides a bridge between Rack's environment hash and Protocol::HTTP::Request.
|
16
|
+
# It handles conversion of Rack environment variables to HTTP request properties.
|
14
17
|
class Request < ::Protocol::HTTP::Request
|
18
|
+
# Get or create a Request instance for the given Rack environment.
|
19
|
+
# The request is cached in the environment to avoid creating multiple instances.
|
20
|
+
#
|
21
|
+
# @parameter env [Hash] The Rack environment hash.
|
22
|
+
# @returns [Request] A Request instance for the environment.
|
15
23
|
def self.[](env)
|
16
24
|
env["protocol.http.request"] ||= new(env)
|
17
25
|
end
|
18
26
|
|
27
|
+
# Initialize a new Request instance from a Rack environment.
|
28
|
+
#
|
29
|
+
# @parameter env [Hash] The Rack environment hash.
|
19
30
|
def initialize(env)
|
20
31
|
@env = env
|
21
32
|
|
@@ -31,6 +42,11 @@ module Protocol
|
|
31
42
|
)
|
32
43
|
end
|
33
44
|
|
45
|
+
# Extract the protocol list from the Rack environment.
|
46
|
+
# Checks both `rack.protocol` and `HTTP_UPGRADE` headers.
|
47
|
+
#
|
48
|
+
# @parameter env [Hash] The Rack environment hash.
|
49
|
+
# @returns [Array(String) | Nil] The list of protocols or `nil` if none specified.
|
34
50
|
def self.protocol(env)
|
35
51
|
if protocols = env["rack.protocol"]
|
36
52
|
return Array(protocols)
|
@@ -39,6 +55,11 @@ module Protocol
|
|
39
55
|
end
|
40
56
|
end
|
41
57
|
|
58
|
+
# Extract HTTP headers from the Rack environment.
|
59
|
+
# Converts Rack's `HTTP_*` environment variables to proper HTTP headers.
|
60
|
+
#
|
61
|
+
# @parameter env [Hash] The Rack environment hash.
|
62
|
+
# @returns [Protocol::HTTP::Headers] The extracted HTTP headers.
|
42
63
|
def self.headers(env)
|
43
64
|
headers = ::Protocol::HTTP::Headers.new
|
44
65
|
env.each do |key, value|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "protocol/http/body/rewindable"
|
7
7
|
require "protocol/http/middleware"
|
@@ -9,8 +9,12 @@ require "protocol/http/middleware"
|
|
9
9
|
module Protocol
|
10
10
|
module Rack
|
11
11
|
# Content-type driven input buffering, specific to the needs of `rack`.
|
12
|
+
# This middleware ensures that request bodies for certain content types
|
13
|
+
# can be read multiple times, which is required by Rack's specification.
|
12
14
|
class Rewindable < ::Protocol::HTTP::Middleware
|
13
15
|
# Media types that require buffering.
|
16
|
+
# These types typically contain form data or file uploads that may need
|
17
|
+
# to be read multiple times by Rack applications.
|
14
18
|
BUFFERED_MEDIA_TYPES = %r{
|
15
19
|
application/x-www-form-urlencoded|
|
16
20
|
multipart/form-data|
|
@@ -18,17 +22,23 @@ module Protocol
|
|
18
22
|
multipart/mixed
|
19
23
|
}x
|
20
24
|
|
25
|
+
# The HTTP POST method.
|
21
26
|
POST = "POST"
|
22
27
|
|
23
28
|
# Initialize the rewindable middleware.
|
29
|
+
#
|
24
30
|
# @parameter app [Protocol::HTTP::Middleware] The middleware to wrap.
|
25
31
|
def initialize(app)
|
26
32
|
super(app)
|
27
33
|
end
|
28
34
|
|
29
35
|
# Determine whether the request needs a rewindable body.
|
30
|
-
#
|
31
|
-
#
|
36
|
+
# A request needs a rewindable body if:
|
37
|
+
# - It's a POST request with no content type (legacy behavior)
|
38
|
+
# - It has a content type that matches BUFFERED_MEDIA_TYPES
|
39
|
+
#
|
40
|
+
# @parameter request [Protocol::HTTP::Request] The request to check.
|
41
|
+
# @returns [Boolean] True if the request body should be rewindable.
|
32
42
|
def needs_rewind?(request)
|
33
43
|
content_type = request.headers["content-type"]
|
34
44
|
|
@@ -43,13 +53,20 @@ module Protocol
|
|
43
53
|
return false
|
44
54
|
end
|
45
55
|
|
56
|
+
# Create a Rack environment from the request.
|
57
|
+
# Delegates to the wrapped middleware.
|
58
|
+
#
|
59
|
+
# @parameter request [Protocol::HTTP::Request] The request to create an environment from.
|
60
|
+
# @returns [Hash] The Rack environment hash.
|
46
61
|
def make_environment(request)
|
47
62
|
@delegate.make_environment(request)
|
48
63
|
end
|
49
64
|
|
50
65
|
# Wrap the request body in a rewindable buffer if required.
|
51
|
-
#
|
52
|
-
#
|
66
|
+
# If the request needs a rewindable body, wraps it in a {Protocol::HTTP::Body::Rewindable}.
|
67
|
+
#
|
68
|
+
# @parameter request [Protocol::HTTP::Request] The request to process.
|
69
|
+
# @returns [Protocol::HTTP::Response] The response from the wrapped middleware.
|
53
70
|
def call(request)
|
54
71
|
if body = request.body and needs_rewind?(request)
|
55
72
|
request.body = Protocol::HTTP::Body::Rewindable.new(body)
|
data/lib/protocol/rack.rb
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2022-
|
4
|
+
# Copyright, 2022-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require_relative "rack/version"
|
7
7
|
require_relative "rack/adapter"
|
8
8
|
require_relative "rack/request"
|
9
9
|
require_relative "rack/response"
|
10
|
+
|
11
|
+
# @namespace
|
12
|
+
module Protocol
|
13
|
+
# @namespace
|
14
|
+
module Rack
|
15
|
+
end
|
16
|
+
end
|
data/readme.md
CHANGED
@@ -67,6 +67,16 @@ run proc{|env|
|
|
67
67
|
|
68
68
|
Please see the [project releases](https://socketry.github.io/protocol-rack/releases/index) for all releases.
|
69
69
|
|
70
|
+
### v0.13.0
|
71
|
+
|
72
|
+
- 100% test and documentation coverage.
|
73
|
+
- `Protocol::Rack::Input#rewind` now works when the entire input is already read.
|
74
|
+
- `Protocol::Rack::Adapter::Rack2` has stricter validation of the application response.
|
75
|
+
|
76
|
+
### v0.12.0
|
77
|
+
|
78
|
+
- Ignore (and close) response bodies for status codes that don't allow them.
|
79
|
+
|
70
80
|
### v0.11.2
|
71
81
|
|
72
82
|
- Stop setting `env["SERVER_PORT"]` to `nil` if not present.
|
data/releases.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
# Releases
|
2
2
|
|
3
|
+
## v0.13.0
|
4
|
+
|
5
|
+
- 100% test and documentation coverage.
|
6
|
+
- {Protocol::Rack::Input\#rewind} now works when the entire input is already read.
|
7
|
+
- {Protocol::Rack::Adapter::Rack2} has stricter validation of the application response.
|
8
|
+
|
9
|
+
## v0.12.0
|
10
|
+
|
11
|
+
- Ignore (and close) response bodies for status codes that don't allow them.
|
12
|
+
|
3
13
|
## v0.11.2
|
4
14
|
|
5
15
|
- Stop setting `env["SERVER_PORT"]` to `nil` if not present.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: protocol-rack
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.13.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -38,7 +38,7 @@ cert_chain:
|
|
38
38
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
39
39
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
40
40
|
-----END CERTIFICATE-----
|
41
|
-
date: 2025-
|
41
|
+
date: 2025-05-10 00:00:00.000000000 Z
|
42
42
|
dependencies:
|
43
43
|
- !ruby/object:Gem::Dependency
|
44
44
|
name: protocol-http
|
metadata.gz.sig
CHANGED
Binary file
|