Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Please see the [project releases](https://socketry.github.io/protocol-http/relea
### Unreleased

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

## See Also

Expand Down
23 changes: 23 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,28 @@ client.get("/", headers: {"accept" => "text/html"}, authority: "example.com")
# Response keyword arguments:
def call(request)
return Response[200, headers: {"content-Type" => "text/html"}, body: "Hello, World!"]
```

### 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)
if request.headers["expect"] == "100-continue"
# Send an interim response:
request.send_interim_response(100)
end

# ...
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