Skip to content

Commit

Permalink
Allow http_parser to decide if there is still body to read
Browse files Browse the repository at this point in the history
It mostly fixes problem when connection which uses "Transfer-Encoding: chunked"
header is closed to early before the terminating chunk is received. In such
case "http" returned partial body instead of raising error.
  • Loading branch information
Piotr Boniecki committed Aug 3, 2017
1 parent 256539e commit e101a4d
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 10 deletions.
1 change: 1 addition & 0 deletions lib/http/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ def read_more(size)

value = @socket.readpartial(size)
if value == :eof
@parser << ""
:eof
elsif value
@parser << value
Expand Down
2 changes: 1 addition & 1 deletion lib/http/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def initialize(opts)
encoding = opts[:encoding] || charset || Encoding::BINARY
stream = body_stream_for(connection, opts)

@body = Response::Body.new(stream, :encoding => encoding, :length => content_length)
@body = Response::Body.new(stream, :encoding => encoding)
else
@body = opts.fetch(:body)
end
Expand Down
8 changes: 2 additions & 6 deletions lib/http/response/body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,12 @@ class Body
# @return [HTTP::Connection]
attr_reader :connection

def initialize(stream, length: nil, encoding: Encoding::BINARY)
def initialize(stream, encoding: Encoding::BINARY)
@stream = stream
@connection = stream.is_a?(Inflater) ? stream.connection : stream
@streaming = nil
@contents = nil
@encoding = find_encoding(encoding)
@length = length || Float::INFINITY
end

# (see HTTP::Client#readpartial)
Expand All @@ -48,10 +47,7 @@ def to_s
@streaming = false
@contents = String.new("").force_encoding(@encoding)

length = @length

while length > 0 && (chunk = @stream.readpartial)
length -= chunk.bytesize
while (chunk = @stream.readpartial)
@contents << chunk.force_encoding(@encoding)
end
rescue
Expand Down
55 changes: 54 additions & 1 deletion spec/lib/http/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ def simple_response(body, status = 200)

allow(socket_spy).to receive(:close) { nil }
allow(socket_spy).to receive(:closed?) { true }
allow(socket_spy).to receive(:readpartial) { chunks[0] }
allow(socket_spy).to receive(:readpartial) { chunks.shift || :eof }
allow(socket_spy).to receive(:write) { chunks[0].length }

allow(TCPSocket).to receive(:open) { socket_spy }
Expand All @@ -338,5 +338,58 @@ def simple_response(body, status = 200)
expect(body).to eq "<!doctype html>"
end
end

context "when uses chunked transfer encoding" do
let(:chunks) do
[
<<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n") << body
| HTTP/1.1 200 OK
| Content-Type: application/json
| Transfer-Encoding: chunked
| Connection: close
|
RESPONSE
]
end
let(:body) do
<<-BODY.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
| 9
| {"state":
| 5
| "ok"}
| 0
|
BODY
end

before do
socket_spy = double

allow(socket_spy).to receive(:close) { nil }
allow(socket_spy).to receive(:closed?) { true }
allow(socket_spy).to receive(:readpartial) { chunks.shift || :eof }
allow(socket_spy).to receive(:write) { chunks[0].length }

allow(TCPSocket).to receive(:open) { socket_spy }
end

it "properly reads body" do
body = client.get(dummy.endpoint).to_s
expect(body).to eq '{"state":"ok"}'
end

context "with broken body (too early closed connection)" do
let(:body) do
<<-BODY.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
| 9
| {"state":
BODY
end

it "raises HTTP::ConnectionError" do
expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
end
end
end
end
end
7 changes: 5 additions & 2 deletions spec/lib/http/response/body_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
let(:connection) { double(:sequence_id => 0) }
let(:chunks) { [String.new("Hello, "), String.new("World!")] }

before { allow(connection).to receive(:readpartial) { chunks.shift } }
before do
allow(connection).to receive(:readpartial) { chunks.shift }
allow(connection).to receive(:body_completed?) { chunks.empty? }
end

subject(:body) { described_class.new(connection, :encoding => Encoding::UTF_8) }
subject(:body) { described_class.new(connection, :encoding => Encoding::UTF_8) }

it "streams bodies from responses" do
expect(subject.to_s).to eq("Hello, World!")
Expand Down

0 comments on commit e101a4d

Please sign in to comment.