Feature #6492 » net.http.inflate_by_default.3.patch
| lib/net/http/response.rb (working copy) | ||
|---|---|---|
| 
       private 
   | 
||
| 
       def read_body_0(dest) 
   | 
||
| 
         if chunked? 
   | 
||
| 
           read_chunked dest 
   | 
||
| 
           return 
   | 
||
| 
         end 
   | 
||
| 
         clen = content_length() 
   | 
||
| 
         if clen 
   | 
||
| 
           @socket.read clen, dest, true   # ignore EOF 
   | 
||
| 
           return 
   | 
||
| 
       ## 
   | 
||
| 
       # Checks for a supported Content-Encoding header and yields an Inflate 
   | 
||
| 
       # wrapper for this response's socket when zlib is present.  If the 
   | 
||
| 
       # Content-Encoding is unsupported or zlib is missing the plain socket is 
   | 
||
| 
       # yielded. 
   | 
||
| 
       # 
   | 
||
| 
       # If a Content-Range header is present a plain socket is yielded as the 
   | 
||
| 
       # bytes in the range may not be a complete deflate block. 
   | 
||
| 
       def inflater # :nodoc: 
   | 
||
| 
         return yield @socket unless Net::HTTP::HAVE_ZLIB 
   | 
||
| 
         return yield @socket if self['content-range'] 
   | 
||
| 
         case self['content-encoding'] 
   | 
||
| 
         when 'deflate', 'gzip', 'x-gzip' then 
   | 
||
| 
           self.delete 'content-encoding' 
   | 
||
| 
           inflate_body_io = Inflater.new(@socket) 
   | 
||
| 
           begin 
   | 
||
| 
             yield inflate_body_io 
   | 
||
| 
           ensure 
   | 
||
| 
             inflate_body_io.finish 
   | 
||
| 
           end 
   | 
||
| 
         when 'none', 'identity' then 
   | 
||
| 
           self.delete 'content-encoding' 
   | 
||
| 
           yield @socket 
   | 
||
| 
         else 
   | 
||
| 
           yield @socket 
   | 
||
| 
         end 
   | 
||
| 
         clen = range_length() 
   | 
||
| 
         if clen 
   | 
||
| 
           @socket.read clen, dest 
   | 
||
| 
           return 
   | 
||
| 
       end 
   | 
||
| 
       def read_body_0(dest) 
   | 
||
| 
         inflater do |inflate_body_io| 
   | 
||
| 
           if chunked? 
   | 
||
| 
             read_chunked dest, inflate_body_io 
   | 
||
| 
             return 
   | 
||
| 
           end 
   | 
||
| 
           @socket = inflate_body_io 
   | 
||
| 
           clen = content_length() 
   | 
||
| 
           if clen 
   | 
||
| 
             @socket.read clen, dest, true   # ignore EOF 
   | 
||
| 
             return 
   | 
||
| 
           end 
   | 
||
| 
           clen = range_length() 
   | 
||
| 
           if clen 
   | 
||
| 
             @socket.read clen, dest 
   | 
||
| 
             return 
   | 
||
| 
           end 
   | 
||
| 
           @socket.read_all dest 
   | 
||
| 
         end 
   | 
||
| 
         @socket.read_all dest 
   | 
||
| 
       end 
   | 
||
| 
       def read_chunked(dest) 
   | 
||
| 
       ## 
   | 
||
| 
       # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF, 
   | 
||
| 
       # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip 
   | 
||
| 
       # encoded. 
   | 
||
| 
       # 
   | 
||
| 
       # See RFC 2616 section 3.6.1 for definitions 
   | 
||
| 
       def read_chunked(dest, chunk_data_io) # :nodoc: 
   | 
||
| 
         len = nil 
   | 
||
| 
         total = 0 
   | 
||
| 
         while true 
   | 
||
| ... | ... | |
| 
           len = hexlen.hex 
   | 
||
| 
           break if len == 0 
   | 
||
| 
           begin 
   | 
||
| 
             @socket.read len, dest 
   | 
||
| 
             chunk_data_io.read len, dest 
   | 
||
| 
           ensure 
   | 
||
| 
             total += len 
   | 
||
| 
             @socket.read 2   # \r\n 
   | 
||
| ... | ... | |
| 
       end 
   | 
||
| 
       def procdest(dest, block) 
   | 
||
| 
         raise ArgumentError, 'both arg and block given for HTTP method' \ 
   | 
||
| 
             if dest and block 
   | 
||
| 
         raise ArgumentError, 'both arg and block given for HTTP method' if 
   | 
||
| 
           dest and block 
   | 
||
| 
         if block 
   | 
||
| 
           Net::ReadAdapter.new(block) 
   | 
||
| 
         else 
   | 
||
| ... | ... | |
| 
         end 
   | 
||
| 
       end 
   | 
||
| 
       ## 
   | 
||
| 
       # Inflater is a wrapper around Net::BufferedIO that transparently inflates 
   | 
||
| 
       # zlib and gzip streams. 
   | 
||
| 
       class Inflater # :nodoc: 
   | 
||
| 
         ## 
   | 
||
| 
         # Creates a new Inflater wrapping +socket+ 
   | 
||
| 
         def initialize socket 
   | 
||
| 
           @socket = socket 
   | 
||
| 
           # zlib with automatic gzip detection 
   | 
||
| 
           @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS) 
   | 
||
| 
         end 
   | 
||
| 
         ## 
   | 
||
| 
         # Finishes the inflate stream. 
   | 
||
| 
         def finish 
   | 
||
| 
           @inflate.finish 
   | 
||
| 
         end 
   | 
||
| 
         ## 
   | 
||
| 
         # Returns a Net::ReadAdapter that inflates each read chunk into +dest+. 
   | 
||
| 
         # 
   | 
||
| 
         # This allows a large response body to be inflated without storing the 
   | 
||
| 
         # entire body in memory. 
   | 
||
| 
         def inflate_adapter(dest) 
   | 
||
| 
           block = proc do |chunk| 
   | 
||
| 
             dest << @inflate.inflate(chunk) 
   | 
||
| 
           end 
   | 
||
| 
           Net::ReadAdapter.new(block) 
   | 
||
| 
         end 
   | 
||
| 
         ## 
   | 
||
| 
         # Reads +clen+ bytes from the socket, inflates them, then writes them to 
   | 
||
| 
         # +dest+.  +ignore_eof+ is passed down to Net::BufferedIO#read 
   | 
||
| 
         # 
   | 
||
| 
         # Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes. 
   | 
||
| 
         # At this time there is no way for a user of Net::HTTPResponse to read a 
   | 
||
| 
         # specific number of bytes from the HTTP response body, so this internal 
   | 
||
| 
         # API does not return the same number of bytes as were requested. 
   | 
||
| 
         # 
   | 
||
| 
         # See https://bugs.ruby-lang.org/issues/6492 for further discussion. 
   | 
||
| 
         def read clen, dest, ignore_eof = false 
   | 
||
| 
           temp_dest = inflate_adapter(dest) 
   | 
||
| 
           data = @socket.read clen, temp_dest, ignore_eof 
   | 
||
| 
         end 
   | 
||
| 
         ## 
   | 
||
| 
         # Reads the rest of the socket, inflates it, then writes it to +dest+. 
   | 
||
| 
         def read_all dest 
   | 
||
| 
           temp_dest = inflate_adapter(dest) 
   | 
||
| 
           @socket.read_all temp_dest 
   | 
||
| 
         end 
   | 
||
| 
       end 
   | 
||
| 
     end 
   | 
||
| lib/net/http.rb (working copy) | ||
|---|---|---|
| 
           @use_ssl = false 
   | 
||
| 
           @ssl_context = nil 
   | 
||
| 
           @enable_post_connection_check = true 
   | 
||
| 
           @compression = nil 
   | 
||
| 
           @sspi_enabled = false 
   | 
||
| 
           SSL_IVNAMES.each do |ivname| 
   | 
||
| 
             instance_variable_set ivname, nil 
   | 
||
| ... | ... | |
| 
               initheader = initheader.merge({ 
   | 
||
| 
                 "accept-encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" 
   | 
||
| 
               }) 
   | 
||
| 
               @compression = true 
   | 
||
| 
             end 
   | 
||
| 
           end 
   | 
||
| 
           request(Get.new(path, initheader)) {|r| 
   | 
||
| 
             if r.key?("content-encoding") and @compression 
   | 
||
| 
               @compression = nil # Clear it till next set. 
   | 
||
| 
               the_body = r.read_body dest, &block 
   | 
||
| 
               case r["content-encoding"] 
   | 
||
| 
               when "gzip" 
   | 
||
| 
                 r.body= Zlib::GzipReader.new(StringIO.new(the_body), encoding: "ASCII-8BIT").read 
   | 
||
| 
                 r.delete("content-encoding") 
   | 
||
| 
               when "deflate" 
   | 
||
| 
                 r.body= Zlib::Inflate.inflate(the_body); 
   | 
||
| 
                 r.delete("content-encoding") 
   | 
||
| 
               when "identity" 
   | 
||
| 
                 ; # nothing needed 
   | 
||
| 
               else 
   | 
||
| 
                 ; # Don't do anything dramatic, unless we need to later 
   | 
||
| 
               end 
   | 
||
| 
             else 
   | 
||
| 
               r.read_body dest, &block 
   | 
||
| 
             end 
   | 
||
| 
             r.read_body dest, &block 
   | 
||
| 
             res = r 
   | 
||
| 
           } 
   | 
||
| 
           res 
   | 
||
| test/net/http/test_httpresponse.rb (working copy) | ||
|---|---|---|
| 
     class HTTPResponseTest < Test::Unit::TestCase 
   | 
||
| 
       def test_singleline_header 
   | 
||
| 
         io = dummy_io(<<EOS.gsub(/\n/, "\r\n")) 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Content-Length: 5 
   | 
||
| 
     Connection: close 
   | 
||
| ... | ... | |
| 
       end 
   | 
||
| 
       def test_multiline_header 
   | 
||
| 
         io = dummy_io(<<EOS.gsub(/\n/, "\r\n")) 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     X-Foo: XXX 
   | 
||
| 
        YYY 
   | 
||
| ... | ... | |
| 
         assert_equal('XXX YYY', res.header['x-bar']) 
   | 
||
| 
       end 
   | 
||
| 
       def test_read_body 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Connection: close 
   | 
||
| 
     Content-Length: 5 
   | 
||
| 
     hello 
   | 
||
| 
     EOS 
   | 
||
| 
         res = Net::HTTPResponse.read_new(io) 
   | 
||
| 
         body = nil 
   | 
||
| 
         res.reading_body io, true do 
   | 
||
| 
           body = res.read_body 
   | 
||
| 
         end 
   | 
||
| 
         assert_equal 'hello', body 
   | 
||
| 
       end 
   | 
||
| 
       def test_read_body_block 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Connection: close 
   | 
||
| 
     Content-Length: 5 
   | 
||
| 
     hello 
   | 
||
| 
     EOS 
   | 
||
| 
         res = Net::HTTPResponse.read_new(io) 
   | 
||
| 
         body = '' 
   | 
||
| 
         res.reading_body io, true do 
   | 
||
| 
           res.read_body do |chunk| 
   | 
||
| 
             body << chunk 
   | 
||
| 
           end 
   | 
||
| 
         end 
   | 
||
| 
         assert_equal 'hello', body 
   | 
||
| 
       end 
   | 
||
| 
       def test_read_body_content_encoding_deflate 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Connection: close 
   | 
||
| 
     Content-Encoding: deflate 
   | 
||
| 
     Content-Length: 13 
   | 
||
| 
     x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15 
   | 
||
| 
     EOS 
   | 
||
| 
         res = Net::HTTPResponse.read_new(io) 
   | 
||
| 
         body = nil 
   | 
||
| 
         res.reading_body io, true do 
   | 
||
| 
           body = res.read_body 
   | 
||
| 
         end 
   | 
||
| 
         assert_equal 'hello', body 
   | 
||
| 
       end 
   | 
||
| 
       def test_read_body_content_encoding_deflate_chunked 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Connection: close 
   | 
||
| 
     Content-Encoding: deflate 
   | 
||
| 
     Transfer-Encoding: chunked 
   | 
||
| 
     6 
   | 
||
| 
     x\x9C\xCBH\xCD\xC9 
   | 
||
| 
     7 
   | 
||
| 
     \xC9\a\x00\x06,\x02\x15 
   | 
||
| 
     0 
   | 
||
| 
     EOS 
   | 
||
| 
         res = Net::HTTPResponse.read_new(io) 
   | 
||
| 
         body = nil 
   | 
||
| 
         res.reading_body io, true do 
   | 
||
| 
           body = res.read_body 
   | 
||
| 
         end 
   | 
||
| 
         assert_equal 'hello', body 
   | 
||
| 
       end 
   | 
||
| 
       def test_read_body_content_encoding_deflate_no_length 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Connection: close 
   | 
||
| 
     Content-Encoding: deflate 
   | 
||
| 
     x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15 
   | 
||
| 
     EOS 
   | 
||
| 
         res = Net::HTTPResponse.read_new(io) 
   | 
||
| 
         body = nil 
   | 
||
| 
         res.reading_body io, true do 
   | 
||
| 
           body = res.read_body 
   | 
||
| 
         end 
   | 
||
| 
         assert_equal 'hello', body 
   | 
||
| 
       end 
   | 
||
| 
       def test_read_body_content_encoding_deflate_content_range 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Accept-Ranges: bytes 
   | 
||
| 
     Connection: close 
   | 
||
| 
     Content-Encoding: gzip 
   | 
||
| 
     Content-Length: 10 
   | 
||
| 
     Content-Range: bytes 0-9/55 
   | 
||
| 
     \x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03 
   | 
||
| 
     EOS 
   | 
||
| 
         res = Net::HTTPResponse.read_new(io) 
   | 
||
| 
         body = nil 
   | 
||
| 
         res.reading_body io, true do 
   | 
||
| 
           body = res.read_body 
   | 
||
| 
         end 
   | 
||
| 
         assert_equal "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03", body 
   | 
||
| 
       end 
   | 
||
| 
       def test_read_body_string 
   | 
||
| 
         io = dummy_io(<<EOS) 
   | 
||
| 
     HTTP/1.1 200 OK 
   | 
||
| 
     Connection: close 
   | 
||
| 
     Content-Length: 5 
   | 
||
| 
     hello 
   | 
||
| 
     EOS 
   | 
||
| 
         res = Net::HTTPResponse.read_new(io) 
   | 
||
| 
         body = '' 
   | 
||
| 
         res.reading_body io, true do 
   | 
||
| 
           res.read_body body 
   | 
||
| 
         end 
   | 
||
| 
         assert_equal 'hello', body 
   | 
||
| 
       end 
   | 
||
| 
     private 
   | 
||
| 
       def dummy_io(str) 
   | 
||
| 
         str = str.gsub(/\n/, "\r\n") 
   | 
||
| 
         Net::BufferedIO.new(StringIO.new(str)) 
   | 
||
| 
       end 
   | 
||
| 
     end 
   | 
||