Skip to content

Commit bba8028

Browse files
authored
Support for interim responses. (#63)
1 parent 2ffb639 commit bba8028

File tree

5 files changed

+100
-3
lines changed

5 files changed

+100
-3
lines changed

guides/design-overview/readme.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,21 @@ response.read -> "dlroW olleH"
189189
~~~
190190

191191
The value of this uni-directional flow is that it is natural for the stream to be taken out of the scope imposed by the nested `call(request)` model. However, the user must explicitly close the stream, since it's no longer scoped to the client and/or server.
192+
193+
## Interim Response Handling
194+
195+
Interim responses are responses that are sent before the final response. They are used for things like `103 Early Hints` and `100 Continue`. These responses are sent before the final response, and are used to signal to the client that the server is still processing the request.
196+
197+
```ruby
198+
body = Body::Writable.new
199+
200+
interim_response_callback = proc do |status, headers|
201+
if status == 100
202+
# Continue sending the request body.
203+
body.write("Hello World")
204+
body.close
205+
end
206+
end
207+
208+
response = client.post("/upload", {'expect' => '100-continue'}, body, interim_response: interim_response_callback)
209+
```

lib/protocol/http/request.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module HTTP
2525
class Request
2626
prepend Body::Reader
2727

28-
def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = Headers.new, body = nil, protocol = nil)
28+
def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = Headers.new, body = nil, protocol = nil, interim_response = nil)
2929
@scheme = scheme
3030
@authority = authority
3131
@method = method
@@ -34,6 +34,7 @@ def initialize(scheme = nil, authority = nil, method = nil, path = nil, version
3434
@headers = headers
3535
@body = body
3636
@protocol = protocol
37+
@interim_response = interim_response
3738
end
3839

3940
# @attribute [String] the request scheme, usually `"http"` or `"https"`.
@@ -60,11 +61,30 @@ def initialize(scheme = nil, authority = nil, method = nil, path = nil, version
6061
# @attribute [String | Array(String) | Nil] the request protocol, usually empty, but occasionally `"websocket"` or `"webtransport"`. In HTTP/1, it is used to request a connection upgrade, and in HTTP/2 it is used to indicate a specfic protocol for the stream.
6162
attr_accessor :protocol
6263

64+
# @attribute [Proc] a callback which is called when an interim response is received.
65+
attr_accessor :interim_response
66+
6367
# Send the request to the given connection.
6468
def call(connection)
6569
connection.call(self)
6670
end
6771

72+
# Send an interim response back to the origin of this request, if possible.
73+
def send_interim_response(status, headers)
74+
@interim_response&.call(status, headers)
75+
end
76+
77+
def on_interim_response(&block)
78+
if interim_response = @interim_response
79+
@interim_response = ->(status, headers) do
80+
block.call(status, headers)
81+
interim_response.call(status, headers)
82+
end
83+
else
84+
@interim_response = block
85+
end
86+
end
87+
6888
# Whether this is a HEAD request: no body is expected in the response.
6989
def head?
7090
@method == Methods::HEAD
@@ -81,11 +101,11 @@ def connect?
81101
# @parameter path [String] The path, e.g. `"/index.html"`, `"/search?q=hello"`, etc.
82102
# @parameter headers [Hash] The headers, e.g. `{"accept" => "text/html"}`, etc.
83103
# @parameter body [String | Array(String) | Body::Readable] The body, e.g. `"Hello, World!"`, etc. See {Body::Buffered.wrap} for more information about .
84-
def self.[](method, path, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil)
104+
def self.[](method, path, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil, interim_response: nil)
85105
body = Body::Buffered.wrap(body)
86106
headers = Headers[headers]
87107

88-
self.new(scheme, authority, method, path, nil, headers, body, protocol)
108+
self.new(scheme, authority, method, path, nil, headers, body, protocol, interim_response)
89109
end
90110

91111
# Whether the request can be replayed without side-effects.

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Please see the [project releases](https://socketry.github.io/protocol-http/relea
2525
### Unreleased
2626

2727
- [`Request[]` and `Response[]` Keyword Arguments](https://socketry.github.io/protocol-http/releases/index#request[]-and-response[]-keyword-arguments)
28+
- [Interim Response Handling](https://socketry.github.io/protocol-http/releases/index#interim-response-handling)
2829

2930
## See Also
3031

releases.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,28 @@ client.get("/", headers: {"accept" => "text/html"}, authority: "example.com")
1313
# Response keyword arguments:
1414
def call(request)
1515
return Response[200, headers: {"content-Type" => "text/html"}, body: "Hello, World!"]
16+
```
17+
18+
### Interim Response Handling
19+
20+
The `Request` class now exposes a `#interim_response` attribute which can be used to handle interim responses both on the client side and server side.
21+
22+
On the client side, you can pass a callback using the `interim_response` keyword argument which will be invoked whenever an interim response is received:
23+
24+
```ruby
25+
client = ...
26+
response = client.get("/index", interim_response: proc{|status, headers| ...})
27+
```
28+
29+
On the server side, you can send an interim response using the `#send_interim_response` method:
30+
31+
```ruby
32+
def call(request)
33+
if request.headers["expect"] == "100-continue"
34+
# Send an interim response:
35+
request.send_interim_response(100)
36+
end
37+
38+
# ...
1639
end
1740
```

test/protocol/http/request.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,39 @@
121121
request.call(connection)
122122
end
123123
end
124+
125+
with "interim response" do
126+
let(:request) {subject.new("http", "localhost", "GET", "/index.html", "HTTP/1.0", headers, body)}
127+
128+
it "should call block" do
129+
request.on_interim_response do |status, headers|
130+
expect(status).to be == 100
131+
expect(headers).to be == {}
132+
end
133+
134+
request.send_interim_response(100, {})
135+
end
136+
137+
it "calls multiple blocks" do
138+
sequence = []
139+
140+
request.on_interim_response do |status, headers|
141+
sequence << 1
142+
143+
expect(status).to be == 100
144+
expect(headers).to be == {}
145+
end
146+
147+
request.on_interim_response do |status, headers|
148+
sequence << 2
149+
150+
expect(status).to be == 100
151+
expect(headers).to be == {}
152+
end
153+
154+
request.send_interim_response(100, {})
155+
156+
expect(sequence).to be == [2, 1]
157+
end
158+
end
124159
end

0 commit comments

Comments
 (0)