protocol-http 0.51.1 → 0.52.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.
@@ -0,0 +1,330 @@
1
+ # Message Body
2
+
3
+ This guide explains how to work with HTTP request and response message bodies using `Protocol::HTTP::Body` classes.
4
+
5
+ ## Overview
6
+
7
+ HTTP message bodies represent the actual (often stateful) data content of requests and responses. `Protocol::HTTP` provides a rich set of body classes for different use cases, from simple string content to streaming data and file serving.
8
+
9
+ All body classes inherit from {ruby Protocol::HTTP::Body::Readable}, which provides a consistent interface for reading data in chunks. Bodies can be:
10
+ - **Buffered**: All content stored in memory.
11
+ - **Streaming**: Content generated or read on-demand.
12
+ - **File-based**: Content read directly from files.
13
+ - **Transforming**: Content modified as it flows through e.g. compression, encryption.
14
+
15
+ ## Core Body Interface
16
+
17
+ Every body implements the `Readable` interface:
18
+
19
+ ``` ruby
20
+ # Read the next chunk of data:
21
+ chunk = body.read
22
+ # => "Hello" or nil when finished
23
+
24
+ # Check if body has data available without blocking:
25
+ body.ready? # => true/false
26
+
27
+ # Check if body is empty:
28
+ body.empty? # => true/false
29
+
30
+ # Close the body and release resources:
31
+ body.close
32
+
33
+ # Iterate through all chunks:
34
+ body.each do |chunk|
35
+ puts chunk
36
+ end
37
+
38
+ # Read entire body into a string:
39
+ content = body.join
40
+ ```
41
+
42
+ ## Buffered Bodies
43
+
44
+ Use {ruby Protocol::HTTP::Body::Buffered} for content that's fully loaded in memory:
45
+
46
+ ``` ruby
47
+ # Create from string:
48
+ body = Protocol::HTTP::Body::Buffered.new(["Hello", " ", "World"])
49
+
50
+ # Create from array of strings:
51
+ chunks = ["First chunk", "Second chunk", "Third chunk"]
52
+ body = Protocol::HTTP::Body::Buffered.new(chunks)
53
+
54
+ # Wrap various types automatically:
55
+ body = Protocol::HTTP::Body::Buffered.wrap("Simple string")
56
+ body = Protocol::HTTP::Body::Buffered.wrap(["Array", "of", "chunks"])
57
+
58
+ # Access properties:
59
+ body.length # => 13 (total size in bytes)
60
+ body.empty? # => false
61
+ body.ready? # => true (always ready)
62
+
63
+ # Reading:
64
+ first_chunk = body.read # => "Hello"
65
+ second_chunk = body.read # => " "
66
+ third_chunk = body.read # => "World"
67
+ fourth_chunk = body.read # => nil (finished)
68
+
69
+ # Rewind to beginning:
70
+ body.rewind
71
+ body.read # => "Hello" (back to start)
72
+ ```
73
+
74
+ ### Buffered Body Features
75
+
76
+ ``` ruby
77
+ # Check if rewindable:
78
+ body.rewindable? # => true for buffered bodies
79
+
80
+ # Get all content as single string:
81
+ content = body.join # => "Hello World"
82
+
83
+ # Convert to array of chunks:
84
+ chunks = body.to_a # => ["Hello", " ", "World"]
85
+
86
+ # Write additional chunks:
87
+ body.write("!")
88
+ body.join # => "Hello World!"
89
+
90
+ # Clear all content:
91
+ body.clear
92
+ body.empty? # => true
93
+ ```
94
+
95
+ ## File Bodies
96
+
97
+ Use {ruby Protocol::HTTP::Body::File} for serving files efficiently:
98
+
99
+ ``` ruby
100
+ require 'protocol/http/body/file'
101
+
102
+ # Open a file:
103
+ body = Protocol::HTTP::Body::File.open("/path/to/file.txt")
104
+
105
+ # Create from existing File object:
106
+ file = File.open("/path/to/image.jpg", "rb")
107
+ body = Protocol::HTTP::Body::File.new(file)
108
+
109
+ # Serve partial content (ranges):
110
+ range = 100...200 # bytes 100-199
111
+ body = Protocol::HTTP::Body::File.new(file, range)
112
+
113
+ # Properties:
114
+ body.length # => file size or range size
115
+ body.empty? # => false (unless zero-length file)
116
+ body.ready? # => false (may block when reading)
117
+
118
+ # File bodies read in chunks automatically:
119
+ body.each do |chunk|
120
+ # Process each chunk (typically 64KB)
121
+ puts "Read #{chunk.bytesize} bytes"
122
+ end
123
+ ```
124
+
125
+ ### File Body Range Requests
126
+
127
+ ``` ruby
128
+ # Serve specific byte ranges (useful for HTTP range requests):
129
+ file = File.open("large_video.mp4", "rb")
130
+
131
+ # First 1MB:
132
+ partial_body = Protocol::HTTP::Body::File.new(file, 0...1_048_576)
133
+
134
+ # Custom block size for reading:
135
+ body = Protocol::HTTP::Body::File.new(file, block_size: 8192) # 8KB chunks
136
+ ```
137
+
138
+ ## Writable Bodies
139
+
140
+ Use {ruby Protocol::HTTP::Body::Writable} for dynamic content generation:
141
+
142
+ ``` ruby
143
+ require 'protocol/http/body/writable'
144
+
145
+ # Create a writable body:
146
+ body = Protocol::HTTP::Body::Writable.new
147
+
148
+ # Write data in another thread/fiber:
149
+ Thread.new do
150
+ body.write("First chunk\n")
151
+ sleep 0.1
152
+ body.write("Second chunk\n")
153
+ body.write("Final chunk\n")
154
+ body.close_write # Signal no more data
155
+ end
156
+
157
+ # Read from main thread:
158
+ body.each do |chunk|
159
+ puts "Received: #{chunk}"
160
+ end
161
+ # Output:
162
+ # Received: First chunk
163
+ # Received: Second chunk
164
+ # Received: Final chunk
165
+ ```
166
+
167
+ ### Writable Body with Backpressure
168
+
169
+ ``` ruby
170
+ # Use SizedQueue to limit buffering:
171
+ queue = Thread::SizedQueue.new(10) # Buffer up to 10 chunks
172
+ body = Protocol::HTTP::Body::Writable.new(queue: queue)
173
+
174
+ # Writing will block if queue is full:
175
+ body.write("chunk 1")
176
+ # ... write up to 10 chunks before blocking
177
+ ```
178
+
179
+ ## Streaming Bodies
180
+
181
+ Use {ruby Protocol::HTTP::Body::Streamable} for computed content:
182
+
183
+ ``` ruby
184
+ require 'protocol/http/body/streamable'
185
+
186
+ # Generate content dynamically:
187
+ body = Protocol::HTTP::Body::Streamable.new do |output|
188
+ 10.times do |i|
189
+ output.write("Line #{i}\n")
190
+ # Could include delays, computation, database queries, etc.
191
+ end
192
+ end
193
+
194
+ # Content is generated as it's read:
195
+ body.each do |chunk|
196
+ puts "Got: #{chunk}"
197
+ end
198
+ ```
199
+
200
+ ## Stream Bodies (IO Wrapper)
201
+
202
+ Use {ruby Protocol::HTTP::Body::Stream} to wrap IO-like objects:
203
+
204
+ ``` ruby
205
+ require 'protocol/http/body/stream'
206
+
207
+ # Wrap an IO object:
208
+ io = StringIO.new("Hello\nWorld\nFrom\nStream")
209
+ body = Protocol::HTTP::Body::Stream.new(io)
210
+
211
+ # Read line by line:
212
+ line1 = body.gets # => "Hello\n"
213
+ line2 = body.gets # => "World\n"
214
+
215
+ # Read specific amounts:
216
+ data = body.read(5) # => "From\n"
217
+
218
+ # Read remaining data:
219
+ rest = body.read # => "Stream"
220
+ ```
221
+
222
+ ## Body Transformations
223
+
224
+ ### Compression Bodies
225
+
226
+ ``` ruby
227
+ require 'protocol/http/body/deflate'
228
+ require 'protocol/http/body/inflate'
229
+
230
+ # Compress a body:
231
+ original = Protocol::HTTP::Body::Buffered.new(["Hello World"])
232
+ compressed = Protocol::HTTP::Body::Deflate.new(original)
233
+
234
+ # Decompress a body:
235
+ decompressed = Protocol::HTTP::Body::Inflate.new(compressed)
236
+ content = decompressed.join # => "Hello World"
237
+ ```
238
+
239
+ ### Wrapper Bodies
240
+
241
+ Create custom body transformations:
242
+
243
+ ``` ruby
244
+ require 'protocol/http/body/wrapper'
245
+
246
+ class UppercaseBody < Protocol::HTTP::Body::Wrapper
247
+ def read
248
+ if chunk = super
249
+ chunk.upcase
250
+ end
251
+ end
252
+ end
253
+
254
+ # Use the wrapper:
255
+ original = Protocol::HTTP::Body::Buffered.wrap("hello world")
256
+ uppercase = UppercaseBody.new(original)
257
+ content = uppercase.join # => "HELLO WORLD"
258
+ ```
259
+
260
+ ## Life-cycle
261
+
262
+ ### Initialization
263
+
264
+ Bodies are typically initialized with the data they need to process. For example:
265
+
266
+ ``` ruby
267
+ body = Protocol::HTTP::Body::Buffered.wrap("Hello World")
268
+ ```
269
+
270
+ ### Reading
271
+
272
+ Once initialized, bodies can be read in chunks:
273
+
274
+ ``` ruby
275
+ body.each do |chunk|
276
+ puts "Read #{chunk.bytesize} bytes"
277
+ end
278
+ ```
279
+
280
+ ### Closing
281
+
282
+ It's important to close bodies when done to release resources:
283
+
284
+ ``` ruby
285
+ begin
286
+ # ... read from the body ...
287
+ rescue => error
288
+ # Ignore.
289
+ ensure
290
+ # The body should always be closed:
291
+ body.close(error)
292
+ end
293
+ ```
294
+
295
+ ## Advanced Usage
296
+
297
+ ### Rewindable Bodies
298
+
299
+ Make any body rewindable by buffering:
300
+
301
+ ``` ruby
302
+ require 'protocol/http/body/rewindable'
303
+
304
+ # Wrap a non-rewindable body:
305
+ file_body = Protocol::HTTP::Body::File.open("data.txt")
306
+ rewindable = Protocol::HTTP::Body::Rewindable.new(file_body)
307
+
308
+ # Read some data:
309
+ first_chunk = rewindable.read
310
+
311
+ # Rewind and read again:
312
+ rewindable.rewind
313
+ same_chunk = rewindable.read # Same as first_chunk
314
+ ```
315
+
316
+ ### Head Bodies (Response without content)
317
+
318
+ For HEAD requests that need content-length but no body:
319
+
320
+ ``` ruby
321
+ require 'protocol/http/body/head'
322
+
323
+ # Create head body from another body:
324
+ original = Protocol::HTTP::Body::File.open("large_file.zip")
325
+ head_body = Protocol::HTTP::Body::Head.for(original)
326
+
327
+ head_body.length # => size of original file
328
+ head_body.read # => nil (no actual content)
329
+ head_body.empty? # => true
330
+ ```
@@ -0,0 +1,195 @@
1
+ # Middleware
2
+
3
+ This guide explains how to build and use HTTP middleware with `Protocol::HTTP::Middleware`.
4
+
5
+ ## Overview
6
+
7
+ The middleware interface provides a convenient wrapper for implementing HTTP middleware components that can process requests and responses. Middleware enables you to build composable HTTP applications by chaining multiple processing layers.
8
+
9
+ A middleware instance generally needs to respond to two methods:
10
+ - `call(request)` -> `response`.
11
+ - `close()` (called when shutting down).
12
+
13
+ ## Basic Middleware Interface
14
+
15
+ You can implement middleware without using the `Middleware` class by implementing the interface directly:
16
+
17
+ ``` ruby
18
+ class SimpleMiddleware
19
+ def initialize(delegate)
20
+ @delegate = delegate
21
+ end
22
+
23
+ def call(request)
24
+ # Process request here
25
+ response = @delegate.call(request)
26
+ # Process response here
27
+ return response
28
+ end
29
+
30
+ def close
31
+ @delegate&.close
32
+ end
33
+ end
34
+ ```
35
+
36
+ ## Using the Middleware Class
37
+
38
+ The `Protocol::HTTP::Middleware` class provides a convenient base for building middleware:
39
+
40
+ ``` ruby
41
+ require 'protocol/http/middleware'
42
+
43
+ class LoggingMiddleware < Protocol::HTTP::Middleware
44
+ def call(request)
45
+ puts "Processing: #{request.method} #{request.path}"
46
+
47
+ response = super # Calls @delegate.call(request)
48
+
49
+ puts "Response: #{response.status}"
50
+ return response
51
+ end
52
+ end
53
+
54
+ # Use with a delegate:
55
+ app = LoggingMiddleware.new(Protocol::HTTP::Middleware::HelloWorld)
56
+ ```
57
+
58
+ ## Building Middleware Stacks
59
+
60
+ Use `Protocol::HTTP::Middleware.build` to construct middleware stacks:
61
+
62
+ ``` ruby
63
+ require 'protocol/http/middleware'
64
+
65
+ app = Protocol::HTTP::Middleware.build do
66
+ use LoggingMiddleware
67
+ use CompressionMiddleware
68
+ run Protocol::HTTP::Middleware::HelloWorld
69
+ end
70
+
71
+ # Handle a request:
72
+ request = Protocol::HTTP::Request["GET", "/"]
73
+ response = app.call(request)
74
+ ```
75
+
76
+ The builder works by:
77
+ - `use` adds middleware to the stack
78
+ - `run` specifies the final application (defaults to `NotFound`)
79
+ - Middleware is chained in reverse order (last `use` wraps first)
80
+
81
+ ## Block-Based Middleware
82
+
83
+ Convert a block into middleware using `Middleware.for`:
84
+
85
+ ``` ruby
86
+ middleware = Protocol::HTTP::Middleware.for do |request|
87
+ if request.path == '/health'
88
+ Protocol::HTTP::Response[200, {}, ["OK"]]
89
+ else
90
+ # This would normally delegate, but this example doesn't have a delegate
91
+ Protocol::HTTP::Response[404]
92
+ end
93
+ end
94
+
95
+ request = Protocol::HTTP::Request["GET", "/health"]
96
+ response = middleware.call(request)
97
+ # => Response with status 200
98
+ ```
99
+
100
+ ## Built-in Middleware
101
+
102
+ ### HelloWorld
103
+
104
+ Always returns "Hello World!" response:
105
+
106
+ ``` ruby
107
+ app = Protocol::HTTP::Middleware::HelloWorld
108
+ response = app.call(request)
109
+ # => 200 "Hello World!"
110
+ ```
111
+
112
+ ### NotFound
113
+
114
+ Always returns 404 response:
115
+
116
+ ``` ruby
117
+ app = Protocol::HTTP::Middleware::NotFound
118
+ response = app.call(request)
119
+ # => 404 Not Found
120
+ ```
121
+
122
+ ### Okay
123
+
124
+ Always returns 200 response with no body:
125
+
126
+ ``` ruby
127
+ app = Protocol::HTTP::Middleware::Okay
128
+ response = app.call(request)
129
+ # => 200 OK
130
+ ```
131
+
132
+ ## Real-World Middleware Examples
133
+
134
+ ### Authentication Middleware
135
+
136
+ ``` ruby
137
+ class AuthenticationMiddleware < Protocol::HTTP::Middleware
138
+ def initialize(delegate, api_key: nil)
139
+ super(delegate)
140
+ @api_key = api_key
141
+ end
142
+
143
+ def call(request)
144
+ auth_header = request.headers['authorization']
145
+
146
+ unless auth_header == "Bearer #{@api_key}"
147
+ return Protocol::HTTP::Response[401, {}, ["Unauthorized"]]
148
+ end
149
+
150
+ super
151
+ end
152
+ end
153
+
154
+ # Usage:
155
+ app = Protocol::HTTP::Middleware.build do
156
+ use AuthenticationMiddleware, api_key: "secret123"
157
+ run MyApplication
158
+ end
159
+ ```
160
+
161
+ ### Content Type Middleware
162
+
163
+ ``` ruby
164
+ class ContentTypeMiddleware < Protocol::HTTP::Middleware
165
+ def call(request)
166
+ response = super
167
+
168
+ # Add content-type header if not present
169
+ unless response.headers.include?('content-type')
170
+ response.headers['content-type'] = 'text/plain'
171
+ end
172
+
173
+ response
174
+ end
175
+ end
176
+ ```
177
+
178
+ ## Testing Middleware
179
+
180
+ ``` ruby
181
+ describe MyMiddleware do
182
+ let(:app) {MyMiddleware.new(Protocol::HTTP::Middleware::Okay)}
183
+
184
+ it "processes requests correctly" do
185
+ request = Protocol::HTTP::Request["GET", "/test"]
186
+ response = app.call(request)
187
+
188
+ expect(response.status).to be == 200
189
+ end
190
+
191
+ it "closes properly" do
192
+ expect { app.close }.not.to raise_exception
193
+ end
194
+ end
195
+ ```
@@ -0,0 +1,132 @@
1
+ # Streaming
2
+
3
+ This guide gives an overview of how to implement streaming requests and responses.
4
+
5
+ ## Independent Uni-directional Streaming
6
+
7
+ The request and response body work independently of each other can stream data in both directions. {ruby Protocol::HTTP::Body::Stream} provides an interface to merge these independent streams into an IO-like interface.
8
+
9
+ ```ruby
10
+ #!/usr/bin/env ruby
11
+
12
+ require 'async'
13
+ require 'async/http/client'
14
+ require 'async/http/server'
15
+ require 'async/http/endpoint'
16
+
17
+ require 'protocol/http/body/stream'
18
+ require 'protocol/http/body/writable'
19
+
20
+ endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
21
+
22
+ Async do
23
+ server = Async::HTTP::Server.for(endpoint) do |request|
24
+ output = Protocol::HTTP::Body::Writable.new
25
+ stream = Protocol::HTTP::Body::Stream.new(request.body, output)
26
+
27
+ Async do
28
+ # Simple echo server:
29
+ while chunk = stream.readpartial(1024)
30
+ stream.write(chunk)
31
+ end
32
+ rescue EOFError
33
+ # Ignore EOF errors.
34
+ ensure
35
+ stream.close
36
+ end
37
+
38
+ Protocol::HTTP::Response[200, {}, output]
39
+ end
40
+
41
+ server_task = Async{server.run}
42
+
43
+ client = Async::HTTP::Client.new(endpoint)
44
+
45
+ input = Protocol::HTTP::Body::Writable.new
46
+ response = client.get("/", body: input)
47
+
48
+ begin
49
+ stream = Protocol::HTTP::Body::Stream.new(response.body, input)
50
+
51
+ stream.write("Hello, ")
52
+ stream.write("World!")
53
+ stream.close_write
54
+
55
+ while chunk = stream.readpartial(1024)
56
+ puts chunk
57
+ end
58
+ rescue EOFError
59
+ # Ignore EOF errors.
60
+ ensure
61
+ stream.close
62
+ end
63
+ ensure
64
+ server_task.stop
65
+ end
66
+ ```
67
+
68
+ This approach works quite well, especially when the input and output bodies are independently compressed, decompressed, or chunked. However, some protocols, notably, WebSockets operate on the raw connection and don't require this level of abstraction.
69
+
70
+ ## Bi-directional Streaming
71
+
72
+ While WebSockets can work on the above streaming interface, it's a bit more convenient to use the streaming interface directly, which gives raw access to the underlying stream where possible.
73
+
74
+ ```ruby
75
+ #!/usr/bin/env ruby
76
+
77
+ require 'async'
78
+ require 'async/http/client'
79
+ require 'async/http/server'
80
+ require 'async/http/endpoint'
81
+
82
+ require 'protocol/http/body/stream'
83
+ require 'protocol/http/body/writable'
84
+
85
+ endpoint = Async::HTTP::Endpoint.parse('http://localhost:3000')
86
+
87
+ Async do
88
+ server = Async::HTTP::Server.for(endpoint) do |request|
89
+ streamable = Protocol::HTTP::Body::Streamable.
90
+ output = Protocol::HTTP::Body::Writable.new
91
+ stream = Protocol::HTTP::Body::Stream.new(request.body, output)
92
+
93
+ Async do
94
+ # Simple echo server:
95
+ while chunk = stream.readpartial(1024)
96
+ stream.write(chunk)
97
+ end
98
+ rescue EOFError
99
+ # Ignore EOF errors.
100
+ ensure
101
+ stream.close
102
+ end
103
+
104
+ Protocol::HTTP::Response[200, {}, output]
105
+ end
106
+
107
+ server_task = Async{server.run}
108
+
109
+ client = Async::HTTP::Client.new(endpoint)
110
+
111
+ input = Protocol::HTTP::Body::Writable.new
112
+ response = client.get("/", body: input)
113
+
114
+ begin
115
+ stream = Protocol::HTTP::Body::Stream.new(response.body, input)
116
+
117
+ stream.write("Hello, ")
118
+ stream.write("World!")
119
+ stream.close_write
120
+
121
+ while chunk = stream.readpartial(1024)
122
+ puts chunk
123
+ end
124
+ rescue EOFError
125
+ # Ignore EOF errors.
126
+ ensure
127
+ stream.close
128
+ end
129
+ ensure
130
+ server_task.stop
131
+ end
132
+ ```