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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/agent.md +145 -0
- data/context/design-overview.md +209 -0
- data/context/getting-started.md +130 -0
- data/context/hypertext-references.md +140 -0
- data/context/index.yaml +36 -0
- data/context/message-body.md +330 -0
- data/context/middleware.md +195 -0
- data/context/streaming.md +132 -0
- data/context/url-parsing.md +130 -0
- data/lib/protocol/http/accept_encoding.rb +1 -1
- data/lib/protocol/http/body/digestable.rb +1 -1
- data/lib/protocol/http/body/inflate.rb +1 -1
- data/lib/protocol/http/body/stream.rb +2 -2
- data/lib/protocol/http/error.rb +1 -1
- data/lib/protocol/http/header/cache_control.rb +1 -1
- data/lib/protocol/http/headers.rb +16 -6
- data/lib/protocol/http/reference.rb +29 -15
- data/lib/protocol/http/request.rb +1 -1
- data/lib/protocol/http/response.rb +1 -0
- data/lib/protocol/http/version.rb +1 -1
- data/readme.md +16 -2
- data/releases.md +6 -0
- data.tar.gz.sig +0 -0
- metadata +10 -1
- metadata.gz.sig +0 -0
@@ -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
|
+
```
|