Skip to content
Open
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
28 changes: 27 additions & 1 deletion lib/protocol/rack/adapter/generic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
require_relative "../response"
require_relative "../rewindable"

require "protocol/http/body/completable"

module Protocol
module Rack
module Adapter
Expand Down Expand Up @@ -177,6 +179,9 @@ def handle_error(env, status, headers, body, error)
def call(request)
env = self.make_environment(request)

# Keep the input we created, in case the application replaces it:
input = env[RACK_INPUT]

status, headers, body = @app.call(env)

# The status must always be an integer:
Expand All @@ -191,11 +196,32 @@ def call(request)

headers, meta = self.wrap_headers(headers)

return Response.wrap(env, status, headers, meta, body, request)
response = Response.wrap(env, status, headers, meta, body, request)

# If the application read part of the request body, close the rest once the response has been sent, otherwise the server will wait for it:
if partially_read?(input)
if response.body
response.body = ::Protocol::HTTP::Body::Completable.new(response.body, proc{input.close})
else
input.close
end
end

return response
rescue => error
input.close if partially_read?(input)

return self.handle_error(env, status, headers, body, error)
end

# Whether the application read part of the given input, but not all of it.
#
# @parameter input [Input | Nil] The input given to the application.
# @returns [Boolean]
def partially_read?(input)
input.is_a?(Input) and input.partially_read?
end

# Generate a suitable response for the given exception.
#
# @parameter exception [Exception] The exception that occurred.
Expand Down
13 changes: 13 additions & 0 deletions lib/protocol/rack/input.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Input
def initialize(body, ...)
@body = body
@closed = false
@reading = false

super(...)
end
Expand Down Expand Up @@ -70,6 +71,16 @@ def rewind
return false
end

# Whether the application has started reading the input.
def reading?
@reading
end

# Whether the application has read part of the input, but not all of it.
def partially_read?
@reading and !closed?
end

# Whether the stream has been closed.
def closed?
@closed or @body.nil?
Expand All @@ -88,6 +99,8 @@ def flush

def sysread(size, buffer)
if @body
@reading = true

# User's may forget to call #close...
if chunk = @body.read
# If the user reads exactly the content length, we close the stream automatically:
Expand Down
75 changes: 75 additions & 0 deletions test/protocol/rack/adapter/rack3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,81 @@
expect(response.read).to be == "Hello World!"
end
end

with "a request body" do
let(:body) {Protocol::HTTP::Body::Buffered.new(["Hello", " ", "World"])}
let(:request) {Protocol::HTTP::Request.new("https", "example.com", "POST", "/", "http/1.1", Protocol::HTTP::Headers.new, body)}

with "that is partially read" do
let(:app) do
->(env) do
@input = env["rack.input"]
@input.read(5)

[400, {}, ["Rejected"]]
end
end

it "closes the input once the response has been sent" do
expect(response.status).to be == 400
expect(@input).not.to be(:closed?)

expect(response.read).to be == "Rejected"

expect(@input).to be(:closed?)
end
end

with "that is partially read and no response body" do
let(:app) do
->(env) do
@input = env["rack.input"]
@input.read(5)

[204, {}, []]
end
end

it "closes the input immediately" do
expect(response.body).to be_nil
expect(@input).to be(:closed?)
end
end

with "that is partially read when the application fails" do
let(:app) do
->(env) do
@input = env["rack.input"]
@input.read(5)

raise StandardError, "Invalid upload"
end
end

it "closes the input" do
expect(response.status).to be == 500
expect(@input).to be(:closed?)
end
end

with "that is not read" do
let(:app) do
->(env) do
@input = env["rack.input"]

[200, {}, ["Ignored"]]
end
end

it "leaves the input for the server" do
expect(response.body).to be_a(Protocol::HTTP::Body::Buffered)

response.close

expect(@input).not.to be(:closed?)
end
end
end
end

with "#make_response" do
Expand Down