Skip to content

Commit

Permalink
Support for interim responses.
Browse files Browse the repository at this point in the history
  • Loading branch information
ioquatix committed Aug 30, 2024
1 parent d0509fd commit 609fef6
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 3 deletions.
18 changes: 18 additions & 0 deletions guides/design-overview/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,21 @@ response.read -> "dlroW olleH"
~~~

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.

## Interim Response Handling

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.

```ruby
body = Body::Writable.new

interim_response_callback = proc do |status, headers|
if status == 100
# Continue sending the request body.
body.write("Hello World")
body.close
end
end

response = client.post("/upload", {'expect' => '100-continue'}, body, interim_response: interim_response_callback)
```
26 changes: 23 additions & 3 deletions lib/protocol/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ module HTTP
class Request
prepend Body::Reader

def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = Headers.new, body = nil, protocol = nil)
def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = Headers.new, body = nil, protocol = nil, interim_response = nil)
@scheme = scheme
@authority = authority
@method = method
Expand All @@ -34,6 +34,7 @@ def initialize(scheme = nil, authority = nil, method = nil, path = nil, version
@headers = headers
@body = body
@protocol = protocol
@interim_response = interim_response
end

# @attribute [String] the request scheme, usually `"http"` or `"https"`.
Expand All @@ -60,11 +61,30 @@ def initialize(scheme = nil, authority = nil, method = nil, path = nil, version
# @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.
attr_accessor :protocol

# @attribute [Proc] a callback which is called when an interim response is received.
attr_accessor :interim_response

# Send the request to the given connection.
def call(connection)
connection.call(self)
end

# Send an interim response back to the origin of this request, if possible.
def send_interim_response(status, headers)
@interim_response&.call(status, headers)
end

def on_interim_response(&block)
if interim_response = @interim_response
@interim_response = ->(status, headers) do
block.call(status, headers)
interim_response.call(status, headers)
end
else
@interim_response = block
end
end

# Whether this is a HEAD request: no body is expected in the response.
def head?
@method == Methods::HEAD
Expand All @@ -81,11 +101,11 @@ def connect?
# @parameter path [String] The path, e.g. `"/index.html"`, `"/search?q=hello"`, etc.
# @parameter headers [Hash] The headers, e.g. `{"accept" => "text/html"}`, etc.
# @parameter body [String | Array(String) | Body::Readable] The body, e.g. `"Hello, World!"`, etc. See {Body::Buffered.wrap} for more information about .
def self.[](method, path, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil)
def self.[](method, path, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil, interim_response: nil)
body = Body::Buffered.wrap(body)
headers = Headers[headers]

self.new(scheme, authority, method, path, nil, headers, body, protocol)
self.new(scheme, authority, method, path, nil, headers, body, protocol, interim_response)
end

# Whether the request can be replayed without side-effects.
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Please see the [project releases](https://socketry.github.io/protocol-http/relea

### Unreleased

- [Interim Response Handling](https://socketry.github.io/protocol-http/releases/index#interim-response-handling)

## See Also

- [protocol-http1](https://github.com/socketry/protocol-http1) — HTTP/1 client/server implementation using this
Expand Down
24 changes: 24 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# Releases

## Unreleased

### Interim Response Handling

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.

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:

```ruby
client = ...
response = client.get("/index", interim_response: proc{|status, headers| ...})
```

On the server side, you can send an interim response using the `#send_interim_response` method:

```ruby
def call(request)
body = Body::Writable.new

# Send an interim response:
request.send_interim_response(100, {})

return Response[200, headers, body]
end
```
35 changes: 35 additions & 0 deletions test/protocol/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,39 @@
request.call(connection)
end
end

with "interim response" do
let(:request) {subject.new("http", "localhost", "GET", "/index.html", "HTTP/1.0", headers, body)}

it "should call block" do
request.on_interim_response do |status, headers|
expect(status).to be == 100
expect(headers).to be == {}
end

request.send_interim_response(100, {})
end

it "calls multiple blocks" do
sequence = []

request.on_interim_response do |status, headers|
sequence << 1

expect(status).to be == 100
expect(headers).to be == {}
end

request.on_interim_response do |status, headers|
sequence << 2

expect(status).to be == 100
expect(headers).to be == {}
end

request.send_interim_response(100, {})

expect(sequence).to be == [2, 1]
end
end
end

0 comments on commit 609fef6

Please sign in to comment.