Skip to content

Commit 5e0d02c

Browse files
committed
Use IO.copy_stream when possible
1 parent 27fbb07 commit 5e0d02c

File tree

4 files changed

+130
-81
lines changed

4 files changed

+130
-81
lines changed

lib/httpclient.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -651,8 +651,8 @@ def redirect_uri_callback=(redirect_uri_callback)
651651
# use get method. get returns HTTP::Message as a response and you need to
652652
# follow HTTP redirect by yourself if you need.
653653
def get_content(uri, *args, &block)
654-
query, header = keyword_argument(args, :query, :header)
655-
success_content(follow_redirect(:get, uri, query, nil, header || {}, &block))
654+
query, header, to = keyword_argument(args, :query, :header, :to)
655+
success_content(follow_redirect(:get, uri, query, nil, header || {}, to, &block))
656656
end
657657

658658
# Posts a content.
@@ -995,7 +995,7 @@ def hashy_argument_has_keys(args, *key)
995995
key.all? { |e| args[0].key?(e) }
996996
end
997997

998-
def do_request(method, uri, query, body, header, &block)
998+
def do_request(method, uri, query, body, header, to = nil, &block)
999999
res = nil
10001000
if HTTP::Message.file?(body)
10011001
pos = body.pos rescue nil
@@ -1016,7 +1016,7 @@ def do_request(method, uri, query, body, header, &block)
10161016
# We want to delete Connection usage in do_get_block but Newrelic gem depends on it.
10171017
# https://github.com/newrelic/rpm/blob/master/lib/new_relic/agent/instrumentation/httpclient.rb#L34-L36
10181018
conn = Connection.new
1019-
res = do_get_block(req, proxy, conn, &block)
1019+
res = do_get_block(req, proxy, conn, to, &block)
10201020
# Webmock's do_get_block returns ConditionVariable
10211021
if !res.respond_to?(:previous)
10221022
res = conn.pop
@@ -1085,7 +1085,7 @@ def adapt_block(&block)
10851085
proc { |r, str| block.call(str) }
10861086
end
10871087

1088-
def follow_redirect(method, uri, query, body, header, &block)
1088+
def follow_redirect(method, uri, query, body, header, to = nil, &block)
10891089
uri = to_resource_url(uri)
10901090
if block
10911091
b = adapt_block(&block)
@@ -1101,7 +1101,7 @@ def follow_redirect(method, uri, query, body, header, &block)
11011101
request_query = query
11021102
while retry_number < @follow_redirect_count
11031103
body.pos = pos if pos
1104-
res = do_request(method, uri, request_query, body, header, &filtered_block)
1104+
res = do_request(method, uri, request_query, body, header, to, &filtered_block)
11051105
res.previous = previous
11061106
if res.redirect?
11071107
if res.header['location'].empty?
@@ -1226,7 +1226,7 @@ def no_proxy?(uri)
12261226

12271227
# !! CAUTION !!
12281228
# Method 'do_get*' runs under MT conditon. Be careful to change.
1229-
def do_get_block(req, proxy, conn, &block)
1229+
def do_get_block(req, proxy, conn, to = nil, &block)
12301230
@request_filter.each do |filter|
12311231
filter.filter_request(req)
12321232
end
@@ -1244,7 +1244,8 @@ def do_get_block(req, proxy, conn, &block)
12441244
@debug_dev << "\n\n= Response\n\n" if @debug_dev
12451245
do_get_header(req, res, sess)
12461246
conn.push(res)
1247-
sess.get_body do |part|
1247+
1248+
sess.get_body(to) do |part|
12481249
set_encoding(part, res.body_encoding)
12491250
if block
12501251
block.call(res, part.dup)

lib/httpclient/session.rb

Lines changed: 77 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929

3030

3131
class HTTPClient
32-
33-
3432
# Represents a Site: protocol scheme, host String and port Number.
3533
class Site
3634
# Protocol scheme.
@@ -574,29 +572,31 @@ def eof?
574572
end
575573
end
576574

577-
def get_body(&block)
578-
begin
579-
read_header if @state == :META
580-
return nil if @state != :DATA
581-
if @transparent_gzip_decompression
582-
block = content_inflater_block(@content_encoding, block)
583-
end
584-
if @chunked
585-
read_body_chunked(&block)
586-
elsif @content_length
587-
read_body_length(&block)
588-
else
589-
read_body_rest(&block)
590-
end
591-
rescue
592-
close
593-
raise
594-
end
595-
if eof?
596-
if @next_connection
597-
@state = :WAIT
598-
else
575+
def get_body(to = nil, &block)
576+
cast_to_io(to, block) do |io|
577+
begin
578+
read_header if @state == :META
579+
return nil if @state != :DATA
580+
if @transparent_gzip_decompression
581+
io = content_inflater(@content_encoding, io)
582+
end
583+
if @chunked
584+
read_body_chunked(io)
585+
elsif @content_length
586+
read_body_length(io)
587+
else
588+
read_body_rest(io)
589+
end
590+
rescue
599591
close
592+
raise
593+
end
594+
if eof?
595+
if @next_connection
596+
@state = :WAIT
597+
else
598+
close
599+
end
600600
end
601601
end
602602
nil
@@ -697,7 +697,7 @@ def first_inflate(body)
697697
end
698698
end
699699

700-
def content_inflater_block(content_encoding, block)
700+
def content_inflater(content_encoding, io)
701701
case content_encoding
702702
when 'gzip', 'x-gzip'
703703
# zlib itself has a functionality to decompress gzip stream.
@@ -706,15 +706,12 @@ def content_inflater_block(content_encoding, block)
706706
# > windowBits can also be greater than 15 for optional gzip decoding. Add 32 to
707707
# > windowBits to enable zlib and gzip decoding with automatic header detection,
708708
# > or add 16 to decode only the gzip format
709-
inflate_stream = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
709+
IOInflater.new(io, Zlib::Inflate.new(Zlib::MAX_WBITS + 32))
710710
when 'deflate'
711-
inflate_stream = LenientInflater.new
711+
IOInflater.new(io, LenientInflater.new)
712712
else
713-
return block
713+
io
714714
end
715-
Proc.new { |buf|
716-
block.call(inflate_stream.inflate(buf))
717-
}
718715
end
719716

720717
def set_header(req)
@@ -872,35 +869,20 @@ def parse_content_header(key, value)
872869
end
873870
end
874871

875-
def read_body_length(&block)
876-
return nil if @content_length == 0
877-
while true
878-
buf = empty_bin_str
879-
maxbytes = @read_block_size
880-
maxbytes = @content_length if maxbytes > @content_length && @content_length > 0
881-
::Timeout.timeout(@receive_timeout, ReceiveTimeoutError) do
882-
begin
883-
@socket.readpartial(maxbytes, buf)
884-
rescue EOFError
885-
close
886-
buf = nil
887-
if @strict_response_size_check
888-
raise BadResponseError.new("EOF while reading rest #{@content_length} bytes")
889-
end
890-
end
891-
end
892-
if buf && buf.bytesize > 0
893-
@content_length -= buf.bytesize
894-
yield buf
895-
else
896-
@content_length = 0
897-
end
898-
return if @content_length == 0
872+
def read_body_length(io)
873+
::Timeout.timeout(@receive_timeout, ReceiveTimeoutError) do
874+
@content_length -= IO.copy_stream(@socket, io, @content_length)
875+
end
876+
877+
if @strict_response_size_check && @content_length > 0
878+
raise BadResponseError.new("EOF while reading rest #{@content_length} bytes")
879+
else
880+
@content_length = 0
899881
end
900882
end
901883

902884
RS = "\r\n"
903-
def read_body_chunked(&block)
885+
def read_body_chunked(io)
904886
buf = empty_bin_str
905887
while true
906888
::Timeout.timeout(@receive_timeout, ReceiveTimeoutError) do
@@ -919,14 +901,14 @@ def read_body_chunked(&block)
919901
@socket.read(2)
920902
end
921903
unless buf.empty?
922-
yield buf
904+
io.write(buf)
923905
end
924906
end
925907
end
926908

927-
def read_body_rest
909+
def read_body_rest(io)
928910
if @readbuf and @readbuf.bytesize > 0
929-
yield @readbuf
911+
io.write(@readbuf)
930912
@readbuf = nil
931913
end
932914
while true
@@ -942,19 +924,52 @@ def read_body_rest
942924
end
943925
end
944926
if buf && buf.bytesize > 0
945-
yield buf
927+
io.write(buf)
946928
else
947929
return
948930
end
949931
end
950932
end
951933

934+
def cast_to_io(to, block)
935+
if to.respond_to?(:write)
936+
yield to
937+
elsif to
938+
File.open(to, 'w+') do |file|
939+
yield file
940+
end
941+
else
942+
yield IOBlockAdapter.new(block)
943+
end
944+
end
945+
946+
class IOInflater
947+
def initialize(io, inflater)
948+
@io = io
949+
@inflater = inflater
950+
end
951+
952+
def write(chunk)
953+
@io.write(@inflater.inflate(chunk))
954+
chunk.bytesize
955+
end
956+
end
957+
958+
class IOBlockAdapter
959+
def initialize(block)
960+
@block = block
961+
end
962+
963+
def write(chunk)
964+
@block.call(chunk)
965+
chunk.bytesize
966+
end
967+
end
968+
952969
def empty_bin_str
953970
str = ''
954971
str.force_encoding('BINARY') if str.respond_to?(:force_encoding)
955972
str
956973
end
957974
end
958-
959-
960975
end

test/helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
require 'stringio'
1616
require 'cgi'
1717
require 'webrick/httputils'
18-
18+
require 'tmpdir'
1919

2020
module Helper
2121
Port = 17171

test/test_httpclient.rb

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,41 @@ def test_get_content_with_block
629629
end
630630
end
631631

632+
def test_get_content_with_path
633+
with_tmp_path do |path|
634+
@client.get_content(serverurl + 'hello', to: path)
635+
assert_equal('hello', File.read(path))
636+
end
637+
end
638+
639+
def test_get_content_with_io
640+
with_tmp_path do |path|
641+
File.open(path, 'w+') do |io|
642+
@client.get_content(serverurl + 'hello', to: io)
643+
end
644+
assert_equal('hello', File.read(path))
645+
end
646+
end
647+
648+
def test_get_gzipped_content_with_io
649+
@client.transparent_gzip_decompression = true
650+
651+
with_tmp_path do |path|
652+
@client.get_content(serverurl + 'compressed?enc=gzip', to: path)
653+
assert_equal('hello', File.read(path))
654+
end
655+
656+
with_tmp_path do |path|
657+
@client.get_content(serverurl + 'compressed?enc=deflate', to: path)
658+
assert_equal('hello', File.read(path))
659+
end
660+
661+
with_tmp_path do |path|
662+
@client.get_content(serverurl + 'compressed?enc=deflate_noheader', to: path)
663+
assert_equal('hello', File.read(path))
664+
end
665+
end
666+
632667
def test_post_content
633668
assert_equal('hello', @client.post_content(serverurl + 'hello'))
634669
assert_equal('hello', @client.post_content(serverurl + 'redirect1'))
@@ -829,16 +864,6 @@ def test_get_with_block_arity_2_and_redirects
829864
assert_nil(res.content)
830865
end
831866

832-
def test_get_with_block_string_recycle
833-
@client.read_block_size = 2
834-
body = []
835-
_res = @client.get(serverurl + 'servlet') { |str|
836-
body << str
837-
}
838-
assert_equal(2, body.size)
839-
assert_equal("get", body.join) # Was "tt" by String object recycle...
840-
end
841-
842867
def test_get_with_block_chunked_string_recycle
843868
server = TCPServer.open('localhost', 0)
844869
server_thread = Thread.new {
@@ -1922,6 +1947,14 @@ def test_tcp_keepalive
19221947

19231948
private
19241949

1950+
def with_tmp_path
1951+
path = File.join(Dir.tmpdir, 'http-client-test')
1952+
File.delete(path) if File.exists?(path)
1953+
yield path
1954+
ensure
1955+
File.delete(path) if File.exists?(path)
1956+
end
1957+
19251958
def check_query_get(query)
19261959
WEBrick::HTTPUtils.parse_query(
19271960
@client.get(serverurl + 'servlet', query).header["x-query"][0]

0 commit comments

Comments
 (0)