protocol-rack 0.16.0 → 0.18.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: 6d1c1680392355d0bf9a1aa9b6b76f2267f9d1f612e4ae194fe5933a95fb4486
4
- data.tar.gz: 8401fd53bbe49d9547f187f06a23ba334eae3814570f041b333c8c3c30aa317b
3
+ metadata.gz: 4fed6d6b2ffc0dc007d343e8f9414af4c61cd7c321cb289b6d218290977feb7f
4
+ data.tar.gz: d02dd7e216b58d3bbe94e5e99b4c71d689ad8de5cb7451a67a81bcd0ade5a55a
5
5
  SHA512:
6
- metadata.gz: 7e3194c1c3ae772b58d15126fa918150b3e04791bfcf24a25ef6314d573392c3b8fdfca9efd7bab47a17ab1fd99d2b9a32323699014c900840024368fdfcabcd
7
- data.tar.gz: ebbcaf7b1368633de01b261c1ad8c6f741f798b74d28070e33d4023887b306acb9a86df5b243f2b3bef737a148663c45a94bd981e6cf3d9757ce82048c4be830
6
+ metadata.gz: df29bd96d8542e75e146d0eec4d7813f7e037fdd84e68bd08ac1f6a4cfca66b3ec176dd3167e31eedb3ae849b44059c23b0fe57a4aea8b4a2c3f55c90755b36f
7
+ data.tar.gz: 6969ef3c4a7fb95083dd57697c5bcf0aee57ec1f9eee6f0b4233af56e4845513c21480cee2abc14daf320d5cf7c5c739b50448bc3d3c6023432292a6544d8876
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,55 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to get started with `protocol-rack` and integrate Rack applications with `Protocol::HTTP` servers.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ```bash
10
+ $ bundle add protocol-rack
11
+ ```
12
+
13
+ ## Core Concepts
14
+
15
+ `protocol-rack` provides a bridge between two HTTP ecosystems:
16
+
17
+ - **Rack**: The standard Ruby web server interface used by frameworks like Rails, Sinatra, and Roda.
18
+ - **`Protocol::HTTP`**: A modern, asynchronous HTTP protocol implementation used by servers like Falcon and Async.
19
+
20
+ The library enables bidirectional integration:
21
+
22
+ - **Application Adapter**: Run existing Rack applications on `Protocol::HTTP` servers (like Falcon).
23
+ - **Server Adapter**: Run `Protocol::HTTP` applications on Rack-compatible servers (like Puma).
24
+
25
+ ## Usage
26
+
27
+ The most common use case is running a Rack application on an asynchronous `Protocol::HTTP` server like [falcon](https://github.com/socketry/falcon). This allows you to leverage the performance benefits of async I/O while using your existing Rack-based application code.
28
+
29
+ ### Running a Rack Application
30
+
31
+ When you have an existing Rack application (like a Rails app, Sinatra app, or any app that follows the Rack specification), you can adapt it to run on `Protocol::HTTP` servers:
32
+
33
+ ```ruby
34
+ require "async"
35
+ require "async/http/server"
36
+ require "async/http/endpoint"
37
+ require "protocol/rack/adapter"
38
+
39
+ # Your existing Rack application:
40
+ app = proc do |env|
41
+ [200, {"content-type" => "text/plain"}, ["Hello World"]]
42
+ end
43
+
44
+ # Create an adapter:
45
+ middleware = Protocol::Rack::Adapter.new(app)
46
+
47
+ # Run on an async server:
48
+ Async do
49
+ endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292")
50
+ server = Async::HTTP::Server.new(middleware, endpoint)
51
+ server.run
52
+ end
53
+ ```
54
+
55
+ The adapter automatically detects your Rack version (v2, v3, or v3.1+) and uses the appropriate implementation, ensuring compatibility without any configuration.
@@ -0,0 +1,16 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: An implementation of the Rack protocol/specification.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/protocol-rack/
7
+ source_code_uri: https://github.com/socketry/protocol-rack.git
8
+ files:
9
+ - path: getting-started.md
10
+ title: Getting Started
11
+ description: This guide explains how to get started with `protocol-rack` and integrate
12
+ Rack applications with `Protocol::HTTP` servers.
13
+ - path: request-response.md
14
+ title: Request and Response Handling
15
+ description: This guide explains how to work with requests and responses when bridging
16
+ between Rack and `Protocol::HTTP`, covering advanced use cases and edge cases.
@@ -0,0 +1,253 @@
1
+ # Request and Response Handling
2
+
3
+ This guide explains how to work with requests and responses when bridging between Rack and `Protocol::HTTP`, covering advanced use cases and edge cases.
4
+
5
+ ## Request Conversion
6
+
7
+ The {ruby Protocol::Rack::Request} class converts Rack environment hashes into rich `Protocol::HTTP` request objects, providing access to modern HTTP features while maintaining compatibility with Rack.
8
+
9
+ ### Basic Request Access
10
+
11
+ ```ruby
12
+ require "protocol/rack/request"
13
+
14
+ run do |env|
15
+ request = Protocol::Rack::Request[env]
16
+
17
+ # Access request properties:
18
+ puts request.method # "GET", "POST", etc.
19
+ puts request.path # "/users/123"
20
+ puts request.url_scheme # "http" or "https"
21
+ puts request.authority # "example.com:80"
22
+ end
23
+ ```
24
+
25
+ ### Headers
26
+
27
+ Headers are automatically extracted from Rack's `HTTP_*` environment variables:
28
+
29
+ ```ruby
30
+ run do |env|
31
+ request = Protocol::Rack::Request[env]
32
+
33
+ # Headers are available as a `Protocol::HTTP::Headers` object:
34
+ user_agent = request.headers["user-agent"]
35
+ content_type = request.headers["content-type"]
36
+
37
+ # Headers are case-insensitive:
38
+ user_agent = request.headers["User-Agent"] # Same as above
39
+ end
40
+ ```
41
+
42
+ The adapter converts Rack's `HTTP_ACCEPT_ENCODING` format to standard HTTP header names (`accept-encoding`).
43
+
44
+ ### Request Body
45
+
46
+ The request body is wrapped in a `Protocol::HTTP`-compatible interface:
47
+
48
+ ```ruby
49
+ run do |env|
50
+ request = Protocol::Rack::Request[env]
51
+
52
+ # Read the entire body:
53
+ body = request.body.read
54
+
55
+ # Or stream it:
56
+ request.body.each do |chunk|
57
+ process_chunk(chunk)
58
+ end
59
+
60
+ # The body supports rewind if the underlying Rack input supports it:
61
+ request.body.rewind
62
+ end
63
+ ```
64
+
65
+ The body wrapper handles Rack's `rack.input` interface, which may or may not support `rewind` depending on the server.
66
+
67
+ ### Query Parameters
68
+
69
+ Query parameters are parsed from the request path:
70
+
71
+ ```ruby
72
+ run do |env|
73
+ request = Protocol::Rack::Request[env]
74
+
75
+ # Access query string:
76
+ query = request.query # "name=value&other=123"
77
+
78
+ # Parse query parameters (if using a helper):
79
+ params = URI.decode_www_form(query).to_h
80
+ end
81
+ ```
82
+
83
+ ### Protocol Upgrades
84
+
85
+ The adapter handles protocol upgrade requests (like WebSockets):
86
+
87
+ ```ruby
88
+ run do |env|
89
+ request = Protocol::Rack::Request[env]
90
+
91
+ # Check for upgrade protocols:
92
+ if protocols = request.protocol
93
+ # protocols is an array: ["websocket"]:
94
+ if protocols.include?("websocket")
95
+ # Handle WebSocket upgrade.
96
+ end
97
+ end
98
+ end
99
+ ```
100
+
101
+ Protocols are extracted from either `rack.protocol` or the `HTTP_UPGRADE` header.
102
+
103
+ ## Response Conversion
104
+
105
+ The {ruby Protocol::Rack::Response} class and {ruby Protocol::Rack::Adapter.make_response} handle converting `Protocol::HTTP` responses back to Rack format.
106
+
107
+ ### Basic Response
108
+
109
+ ```ruby
110
+ require "protocol/rack/adapter"
111
+
112
+ run do |env|
113
+ request = Protocol::Rack::Request[env]
114
+
115
+ # Create a `Protocol::HTTP` response:
116
+ response = Protocol::HTTP::Response[
117
+ 200,
118
+ {"content-type" => "text/html"},
119
+ ["<h1>Hello</h1>"]
120
+ ]
121
+
122
+ # Convert to Rack format:
123
+ Protocol::Rack::Adapter.make_response(env, response)
124
+ end
125
+ ```
126
+
127
+ ### Response Bodies
128
+
129
+ The adapter handles different types of response bodies:
130
+
131
+ #### Enumerable Bodies
132
+
133
+ ```ruby
134
+ # Array bodies:
135
+ response = Protocol::HTTP::Response[
136
+ 200,
137
+ {"content-type" => "text/plain"},
138
+ ["Hello", " ", "World"]
139
+ ]
140
+
141
+ # Enumerable bodies:
142
+ response = Protocol::HTTP::Response[
143
+ 200,
144
+ {"content-type" => "text/plain"},
145
+ Enumerator.new do |yielder|
146
+ yielder << "Chunk 1\n"
147
+ yielder << "Chunk 2\n"
148
+ end
149
+ ]
150
+ ```
151
+
152
+ #### Streaming Bodies
153
+
154
+ ```ruby
155
+ # Streaming response body:
156
+ body = Protocol::HTTP::Body::Buffered.new(["Streaming content"])
157
+
158
+ response = Protocol::HTTP::Response[
159
+ 200,
160
+ {"content-type" => "text/plain"},
161
+ body
162
+ ]
163
+ ```
164
+
165
+ #### File Bodies
166
+
167
+ ```ruby
168
+ # File-based responses:
169
+ body = Protocol::HTTP::Body::File.open("path/to/file.txt")
170
+
171
+ response = Protocol::HTTP::Response[
172
+ 200,
173
+ {"content-type" => "text/plain"},
174
+ body
175
+ ]
176
+ ```
177
+
178
+ ### HEAD Requests
179
+
180
+ The adapter automatically handles HEAD requests by removing response bodies:
181
+
182
+ ```ruby
183
+ run do |env|
184
+ request = Protocol::Rack::Request[env]
185
+
186
+ # Create a response with a body:
187
+ response = Protocol::HTTP::Response[
188
+ 200,
189
+ {"content-type" => "text/html"},
190
+ ["<h1>Full Response</h1>"]
191
+ ]
192
+
193
+ # For HEAD requests, the body is automatically removed:
194
+ Protocol::Rack::Adapter.make_response(env, response)
195
+ end
196
+ ```
197
+
198
+ ### Status Codes Without Bodies
199
+
200
+ Certain status codes (204 No Content, 205 Reset Content, 304 Not Modified) should not include response bodies. The adapter handles this automatically:
201
+
202
+ ```ruby
203
+ response = Protocol::HTTP::Response[
204
+ 204, # No Content
205
+ {},
206
+ ["This body will be removed"]
207
+ ]
208
+
209
+ # The adapter automatically removes the body for 204 responses.
210
+ ```
211
+
212
+ ### Rack-Specific Features
213
+
214
+ #### Hijacking
215
+
216
+ Rack supports response hijacking, which allows taking over the connection:
217
+
218
+ ```ruby
219
+ # In a Rack application:
220
+ [200, {"rack.hijack" => proc{|io| io.write("Hijacked!")}}, []]
221
+
222
+ # The adapter handles hijacking automatically using streaming responses.
223
+ ```
224
+
225
+ #### Response Finished Callbacks
226
+
227
+ Rack 2+ supports `rack.response_finished` callbacks:
228
+
229
+ ```ruby
230
+ env["rack.response_finished"] ||= []
231
+ env["rack.response_finished"] << proc do |env, status, headers, error|
232
+ # Cleanup or logging after response is sent
233
+ puts "Response finished: #{status}"
234
+ end
235
+ ```
236
+
237
+ The adapter invokes these callbacks in reverse order of registration, as specified by the Rack specification.
238
+
239
+ ### Hop Headers
240
+
241
+ HTTP hop-by-hop headers (like `Connection`, `Transfer-Encoding`) are automatically removed from responses, as they should not be forwarded through proxies:
242
+
243
+ ```ruby
244
+ response = Protocol::HTTP::Response[
245
+ 200,
246
+ {
247
+ "content-type" => "text/plain",
248
+ "connection" => "close", # This will be removed
249
+ "transfer-encoding" => "chunked" # This will be removed
250
+ },
251
+ ["Body"]
252
+ ]
253
+ ```
@@ -135,6 +135,38 @@ module Protocol
135
135
  }
136
136
  end
137
137
 
138
+ # Handle errors that occur during request processing. Logs the error, closes any response body, invokes `rack.response_finished` callbacks, and returns an appropriate failure response.
139
+ #
140
+ # The `rack.response_finished` callbacks are invoked in reverse order of registration, as specified by the Rack specification. If a callback raises an exception, it is caught and logged, but does not prevent other callbacks from being invoked.
141
+ #
142
+ # @parameter env [Hash] The Rack environment hash.
143
+ # @parameter status [Integer | Nil] The HTTP status code, if available. May be `nil` if the error occurred before the application returned a response.
144
+ # @parameter headers [Hash | Nil] The response headers, if available. May be `nil` if the error occurred before the application returned a response.
145
+ # @parameter body [Object | Nil] The response body, if available. May be `nil` if the error occurred before the application returned a response.
146
+ # @parameter error [Exception] The exception that occurred during request processing.
147
+ # @returns [Protocol::HTTP::Response] A failure response representing the error.
148
+ def handle_error(env, status, headers, body, error)
149
+ Console.error(self, "Error occurred during request processing:", error)
150
+
151
+ # Close the response body if it exists and supports closing.
152
+ body&.close if body.respond_to?(:close)
153
+
154
+ # Invoke `rack.response_finished` callbacks in reverse order of registration.
155
+ # This ensures that callbacks registered later are invoked first, matching the Rack specification.
156
+ env&.[](RACK_RESPONSE_FINISHED)&.reverse_each do |callback|
157
+ begin
158
+ callback.call(env, status, headers, error)
159
+ rescue => callback_error
160
+ # If a callback raises an exception, log it but continue invoking other callbacks.
161
+ # The Rack specification states that callbacks should not raise exceptions, but we handle
162
+ # this gracefully to prevent one misbehaving callback from breaking others.
163
+ Console.error(self, "Error occurred during response finished callback:", callback_error)
164
+ end
165
+ end
166
+
167
+ return failure_response(error)
168
+ end
169
+
138
170
  # Build a rack `env` from the incoming request and apply it to the rack middleware.
139
171
  #
140
172
  # @parameter request [Protocol::HTTP::Request] The incoming request.
@@ -158,16 +190,8 @@ module Protocol
158
190
  headers, meta = self.wrap_headers(headers)
159
191
 
160
192
  return Response.wrap(env, status, headers, meta, body, request)
161
- rescue => exception
162
- Console.error(self, exception)
163
-
164
- body&.close if body.respond_to?(:close)
165
-
166
- env&.[](RACK_RESPONSE_FINISHED)&.each do |callback|
167
- callback.call(env, status, headers, exception)
168
- end
169
-
170
- return failure_response(exception)
193
+ rescue => error
194
+ return self.handle_error(env, status, headers, body, error)
171
195
  end
172
196
 
173
197
  # Generate a suitable response for the given exception.
@@ -111,12 +111,8 @@ module Protocol
111
111
  # end
112
112
 
113
113
  return Response.wrap(env, status, headers, meta, body, request)
114
- rescue => exception
115
- Console.error(self, exception)
116
-
117
- body&.close if body.respond_to?(:close)
118
-
119
- return failure_response(exception)
114
+ rescue => error
115
+ return self.handle_error(env, status, headers, body, error)
120
116
  end
121
117
 
122
118
  # Process the rack response headers into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
@@ -7,6 +7,7 @@ require_relative "body/streaming"
7
7
  require_relative "body/enumerable"
8
8
  require_relative "constants"
9
9
 
10
+ require "console"
10
11
  require "protocol/http/body/completable"
11
12
  require "protocol/http/body/head"
12
13
 
@@ -103,8 +104,10 @@ module Protocol
103
104
  return body
104
105
  end
105
106
 
106
- # Create a completion callback for response finished handlers.
107
- # The callback is called with any error that occurred during response processing.
107
+ # Create a completion callback for response finished handlers. The callback is called with any error that occurred during response processing.
108
+ #
109
+ # Callbacks are invoked in reverse order of registration, as specified by the Rack specification.
110
+ # If a callback raises an exception, it is caught and logged, but does not prevent other callbacks from being invoked.
108
111
  #
109
112
  # @parameter response_finished [Array] Array of response finished callbacks.
110
113
  # @parameter env [Hash] The Rack environment.
@@ -113,8 +116,16 @@ module Protocol
113
116
  # @returns [Proc] A callback that calls all response finished handlers.
114
117
  def self.completion_callback(response_finished, env, status, headers)
115
118
  proc do |error|
116
- response_finished.each do |callback|
117
- callback.call(env, status, headers, error)
119
+ # Invoke callbacks in reverse order of registration, as specified by the Rack specification.
120
+ response_finished.reverse_each do |callback|
121
+ begin
122
+ callback.call(env, status, headers, error)
123
+ rescue => callback_error
124
+ # If a callback raises an exception, log it but continue invoking other callbacks.
125
+ # The Rack specification states that callbacks should not raise exceptions, but we handle
126
+ # this gracefully to prevent one misbehaving callback from breaking others.
127
+ Console.error(self, "Error occurred during response finished callback:", callback_error)
128
+ end
118
129
  end
119
130
  end
120
131
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module Rack
8
- VERSION = "0.16.0"
8
+ VERSION = "0.18.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -13,31 +13,35 @@ Provides abstractions for working with the Rack specification on top of [`Protoc
13
13
 
14
14
  Please see the [project documentation](https://socketry.github.io/protocol-rack/) for more details.
15
15
 
16
+ - [Getting Started](https://socketry.github.io/protocol-rack/guides/getting-started/index) - This guide explains how to get started with `protocol-rack` and integrate Rack applications with `Protocol::HTTP` servers.
17
+
18
+ - [Request and Response Handling](https://socketry.github.io/protocol-rack/guides/request-response/index) - This guide explains how to work with requests and responses when bridging between Rack and `Protocol::HTTP`, covering advanced use cases and edge cases.
19
+
16
20
  ### Application Adapter
17
21
 
18
22
  Given a rack application, you can adapt it for use on `async-http`:
19
23
 
20
24
  ``` ruby
21
- require 'async'
22
- require 'async/http/server'
23
- require 'async/http/client'
24
- require 'async/http/endpoint'
25
- require 'protocol/rack/adapter'
25
+ require "async"
26
+ require "async/http/server"
27
+ require "async/http/client"
28
+ require "async/http/endpoint"
29
+ require "protocol/rack/adapter"
26
30
 
27
31
  app = proc{|env| [200, {}, ["Hello World"]]}
28
32
  middleware = Protocol::Rack::Adapter.new(app)
29
33
 
30
34
  Async do
31
- endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292")
32
-
33
- server_task = Async(transient: true) do
34
- server = Async::HTTP::Server.new(middleware, endpoint)
35
- server.run
36
- end
37
-
38
- client = Async::HTTP::Client.new(endpoint)
39
- puts client.get("/").read
40
- # "Hello World"
35
+ endpoint = Async::HTTP::Endpoint.parse("http://localhost:9292")
36
+
37
+ server_task = Async(transient: true) do
38
+ server = Async::HTTP::Server.new(middleware, endpoint)
39
+ server.run
40
+ end
41
+
42
+ client = Async::HTTP::Client.new(endpoint)
43
+ puts client.get("/").read
44
+ # "Hello World"
41
45
  end
42
46
  ```
43
47
 
@@ -46,27 +50,36 @@ end
46
50
  While not tested, in theory any Rack compatible server can host `Protocol::HTTP` compatible middlewares.
47
51
 
48
52
  ``` ruby
49
- require 'protocol/http/middleware'
50
- require 'protocol/rack'
53
+ require "protocol/http/middleware"
54
+ require "protocol/rack"
51
55
 
52
56
  # Your native application:
53
57
  middleware = Protocol::HTTP::Middleware::HelloWorld
54
58
 
55
- run proc{|env|
56
- # Convert the rack request to a compatible rich request object:
57
- request = Protocol::Rack::Request[env]
58
-
59
- # Call your application
60
- response = middleware.call(request)
61
-
62
- Protocol::Rack::Adapter.make_response(env, response)
63
- }
59
+ run do |env|
60
+ # Convert the rack request to a compatible rich request object:
61
+ request = Protocol::Rack::Request[env]
62
+
63
+ # Call your application
64
+ response = middleware.call(request)
65
+
66
+ Protocol::Rack::Adapter.make_response(env, response)
67
+ end
64
68
  ```
65
69
 
66
70
  ## Releases
67
71
 
68
72
  Please see the [project releases](https://socketry.github.io/protocol-rack/releases/index) for all releases.
69
73
 
74
+ ### v0.18.0
75
+
76
+ - Correctly invoke `rack.response_finished` in reverse order.
77
+ - Tolerate errors during `rack.response_finished` callbacks.
78
+
79
+ ### v0.17.0
80
+
81
+ - Support `rack.response_finished` in Rack 2 if it's present in the environment.
82
+
70
83
  ### v0.16.0
71
84
 
72
85
  - Hijacked IO is no longer duped, as it's not retained by the original connection, and `SSLSocket` does not support duping.
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
3
+ ## v0.18.0
4
+
5
+ - Correctly invoke `rack.response_finished` in reverse order.
6
+ - Tolerate errors during `rack.response_finished` callbacks.
7
+
8
+ ## v0.17.0
9
+
10
+ - Support `rack.response_finished` in Rack 2 if it's present in the environment.
11
+
3
12
  ## v0.16.0
4
13
 
5
14
  - Hijacked IO is no longer duped, as it's not retained by the original connection, and `SSLSocket` does not support duping.
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.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -86,6 +86,9 @@ executables: []
86
86
  extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
+ - context/getting-started.md
90
+ - context/index.yaml
91
+ - context/request-response.md
89
92
  - lib/protocol/rack.rb
90
93
  - lib/protocol/rack/adapter.rb
91
94
  - lib/protocol/rack/adapter/generic.rb
@@ -125,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
128
  - !ruby/object:Gem::Version
126
129
  version: '0'
127
130
  requirements: []
128
- rubygems_version: 3.6.7
131
+ rubygems_version: 3.6.9
129
132
  specification_version: 4
130
133
  summary: An implementation of the Rack protocol/specification.
131
134
  test_files: []
metadata.gz.sig CHANGED
Binary file