Skip to content
This repository has been archived by the owner on Aug 31, 2022. It is now read-only.

Commit

Permalink
client: add auto_decode option: decode automatically deflate or gzip …
Browse files Browse the repository at this point in the history
…encoded response
  • Loading branch information
rhenium committed Nov 10, 2015
1 parent 0dea106 commit 88763b7
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 17 deletions.
2 changes: 2 additions & 0 deletions lib/plum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "socket"
require "base64"
require "set"
require "zlib"
require "plum/version"
require "plum/errors"
require "plum/binary_string"
Expand All @@ -24,6 +25,7 @@
require "plum/server/http_connection"
require "plum/client"
require "plum/client/response"
require "plum/client/decoders"
require "plum/client/connection"
require "plum/client/client_session"
require "plum/client/legacy_client_session"
Expand Down
3 changes: 2 additions & 1 deletion lib/plum/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class Client
ssl_context: nil,
http2_settings: {},
user_agent: "plum/#{Plum::VERSION}",
auto_decode: true,
}.freeze

attr_reader :host, :port, :config
Expand Down Expand Up @@ -79,7 +80,7 @@ def close
# @param block [Proc] if passed, it will be called when received response headers.
def request(headers, body, options = {}, &block)
raise ArgumentError, ":method and :path headers are required" unless headers[":method"] && headers[":path"]
@session.request(headers, body, options, &block)
@session.request(headers, body, @config.merge(options), &block)
end

# @!method get!
Expand Down
2 changes: 1 addition & 1 deletion lib/plum/client/client_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def request(headers, body, options, &headers_cb)
":scheme" => @config[:scheme]
}.merge(headers)

response = Response.new
response = Response.new(**options)
@responses << response
stream = @plum.open_stream
stream.send_headers(headers, end_stream: !body)
Expand Down
51 changes: 51 additions & 0 deletions lib/plum/client/decoders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Plum
module Decoders
class Base
def decode(chunk)
chunk
end

def finish
end
end

# `deflate` is not just deflate, wrapped by zlib format (RFC 1950)
class Deflate < Base
def initialize
@inflate = Zlib::Inflate.new(Zlib::MAX_WBITS)
end

def decode(chunk)
@inflate.inflate(chunk)
rescue Zlib::Error => e
raise DecoderError.new("failed to decode chunk", e)
end

def finish
@inflate.finish
rescue Zlib::Error => e
raise DecoderError.new("failed to finalize", e)
end
end

class GZip < Base
def initialize
@stream = Zlib::Inflate.new(Zlib::MAX_WBITS + 16)
end

def decode(chunk)
@stream.inflate(chunk)
rescue Zlib::Error => e
raise DecoderError.new("failed to decode chunk", e)
end

def finish
@stream.finish
rescue Zlib::Error => e
raise DecoderError.new("failed to finalize", e)
end
end

DECODERS = { "gzip" => GZip, "deflate" => Deflate }.freeze
end
end
2 changes: 1 addition & 1 deletion lib/plum/client/legacy_client_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def request(headers, body, options, &headers_cb)
end
end

response = Response.new
response = Response.new(**options)
@requests << [response, headers, body, chunked, headers_cb]
consume_queue
response
Expand Down
26 changes: 18 additions & 8 deletions lib/plum/client/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ class Response
attr_reader :headers

# @api private
def initialize
def initialize(auto_decode: true, **options)
@body = Queue.new
@finished = false
@failed = false
@body = []
@auto_decode = auto_decode
end

# Returns the HTTP status code.
Expand Down Expand Up @@ -54,7 +55,7 @@ def on_chunk(&block)
def on_finish(&block)
raise ArgumentError, "block must be given" unless block_given?
if finished?
block.call
yield
else
@on_finish = block
end
Expand All @@ -64,21 +65,20 @@ def on_finish(&block)
# @return [String] the whole response body
def body
raise "Body already read" if @on_chunk
if finished?
@body.join
else
raise "Response body is not complete"
end
raise "Response body is not complete" unless finished?
@body.join
end

# @api private
def _headers(raw_headers)
# response headers should not have duplicates
@headers = raw_headers.to_h.freeze
@decoder = setup_decoder
end

# @api private
def _chunk(chunk)
def _chunk(encoded)
chunk = @decoder.decode(encoded)
if @on_chunk
@on_chunk.call(chunk)
else
Expand All @@ -89,12 +89,22 @@ def _chunk(chunk)
# @api private
def _finish
@finished = true
@decoder.finish
@on_finish.call if @on_finish
end

# @api private
def _fail
@failed = true
end

private
def setup_decoder
if @auto_decode
klass = Decoders::DECODERS[@headers["content-encoding"]]
end
klass ||= Decoders::Base
klass.new
end
end
end
22 changes: 16 additions & 6 deletions lib/plum/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ def http2_error_code
ERROR_CODES[@http2_error_type]
end
end

class RemoteHTTPError < HTTPError; end
class RemoteConnectionError < RemoteHTTPError; end
class RemoteStreamError < RemoteHTTPError; end
class LocalHTTPError < HTTPError; end
class LocalConnectionError < LocalHTTPError; end
class LocalStreamError < LocalHTTPError; end

class LegacyHTTPError < Error
attr_reader :headers, :data, :parser

Expand All @@ -41,10 +49,12 @@ def initialize(headers, data, parser)
end
end

class RemoteHTTPError < HTTPError; end
class RemoteConnectionError < RemoteHTTPError; end
class RemoteStreamError < RemoteHTTPError; end
class LocalHTTPError < HTTPError; end
class LocalConnectionError < LocalHTTPError; end
class LocalStreamError < LocalHTTPError; end
class DecoderError < Error
attr_reader :inner_error

def initialize(message, inner_error = nil)
super(message)
@inner_error = inner_error
end
end
end
54 changes: 54 additions & 0 deletions test/plum/client/test_decoders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require "test_helper"

using Plum::BinaryString
class DecodersTest < Minitest::Test
def test_base_decode
decoder = Decoders::Base.new
assert_equal("abc", decoder.decode("abc"))
end

def test_base_finish
decoder = Decoders::Base.new
decoder.finish
end

def test_deflate_decode
decoder = Decoders::Deflate.new
assert_equal("hello", decoder.decode("\x78\x9c\xcb\x48\xcd\xc9\xc9\x07\x00\x06\x2c\x02\x15"))
end

def test_deflate_decode_error
decoder = Decoders::Deflate.new
assert_raises(DecoderError) {
decoder.decode("\x79\x9c\xcb\x48\xcd\xc9\xc9\x07\x00\x06\x2c\x02\x15")
}
end

def test_deflate_finish_error
decoder = Decoders::Deflate.new
decoder.decode("\x78\x9c\xcb\x48\xcd\xc9\xc9\x07\x00\x06\x2c\x02")
assert_raises(DecoderError) {
decoder.finish
}
end

def test_gzip_decode
decoder = Decoders::GZip.new
assert_equal("hello", decoder.decode("\x1f\x8b\x08\x00\x1a\x96\xe0\x4c\x00\x03\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00"))
end

def test_gzip_decode_error
decoder = Decoders::GZip.new
assert_raises(DecoderError) {
decoder.decode("\x2f\x8b\x08\x00\x1a\x96\xe0\x4c\x00\x03\xcb\x48\xcd\xc9\xc9\x07\x00\x86\xa6\x10\x36\x05\x00\x00\x00")
}
end

def test_gzip_finish_error
decoder = Decoders::GZip.new
decoder.decode("\x1f\x8b\x08\x00\x1a\x96")
assert_raises(DecoderError) {
decoder.finish
}
end
end
5 changes: 5 additions & 0 deletions test/plum/client/test_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
class ResponseTest < Minitest::Test
def test_finished
resp = Response.new
resp._headers({})
assert_equal(false, resp.finished?)
resp._finish
assert_equal(true, resp.finished?)
Expand Down Expand Up @@ -34,6 +35,7 @@ def test_headers

def test_body
resp = Response.new
resp._headers({})
resp._chunk("a")
resp._chunk("b")
resp._finish
Expand All @@ -42,6 +44,7 @@ def test_body

def test_body_not_finished
resp = Response.new
resp._headers({})
resp._chunk("a")
resp._chunk("b")
assert_raises { # TODO
Expand All @@ -51,6 +54,7 @@ def test_body_not_finished

def test_on_chunk
resp = Response.new
resp._headers({})
res = []
resp._chunk("a")
resp._chunk("b")
Expand All @@ -63,6 +67,7 @@ def test_on_chunk

def test_on_finish
resp = Response.new
resp._headers({})
ran = false
resp.on_finish { ran = true }
resp._finish
Expand Down

0 comments on commit 88763b7

Please sign in to comment.