diff --git a/lib/http/chainable.rb b/lib/http/chainable.rb index d5fcd411..650115ee 100644 --- a/lib/http/chainable.rb +++ b/lib/http/chainable.rb @@ -180,6 +180,11 @@ def cookies(cookies) branch default_options.with_cookies(cookies) end + # Force a specific encoding for response body + def encoding(encoding) + branch default_options.with_encoding(encoding) + end + # Accept the given MIME type(s) # @param type def accept(type) diff --git a/lib/http/client.rb b/lib/http/client.rb index 210b375b..7d39dace 100644 --- a/lib/http/client.rb +++ b/lib/http/client.rb @@ -61,11 +61,12 @@ def perform(req, options) end res = Response.new( - @connection.status_code, - @connection.http_version, - @connection.headers, - Response::Body.new(@connection), - req.uri + :status => @connection.status_code, + :version => @connection.http_version, + :headers => @connection.headers, + :connection => @connection, + :encoding => options.encoding, + :uri => req.uri ) @connection.finish_response if req.verb == :head diff --git a/lib/http/options.rb b/lib/http/options.rb index 31ec674e..029f802b 100644 --- a/lib/http/options.rb +++ b/lib/http/options.rb @@ -47,7 +47,8 @@ def initialize(options = {}) :ssl => {}, :keep_alive_timeout => 5, :headers => {}, - :cookies => {} + :cookies => {}, + :encoding => nil } opts_w_defaults = defaults.merge(options) @@ -66,6 +67,10 @@ def initialize(options = {}) end end + def_option :encoding do |encoding| + self.encoding = Encoding.find(encoding) + end + %w( proxy params form json body follow response socket_class ssl_socket_class ssl_context ssl diff --git a/lib/http/response.rb b/lib/http/response.rb index dc6064c4..b99530ca 100644 --- a/lib/http/response.rb +++ b/lib/http/response.rb @@ -23,12 +23,27 @@ class Response # @return [URI, nil] attr_reader :uri - def initialize(status, version, headers, body, uri = nil) # rubocop:disable ParameterLists - @version = version - @body = body - @uri = uri && HTTP::URI.parse(uri) - @status = HTTP::Response::Status.new status - @headers = HTTP::Headers.coerce(headers || {}) + # Inits a new instance + # + # @option opts [Integer] :status Status code + # @option opts [String] :version HTTP version + # @option opts [Hash] :headers + # @option opts [HTTP::Connection] :connection + # @option opts [String] :encoding Encoding to use when reading body + # @option opts [String] :body + # @option opts [String] :uri + def initialize(opts) + @version = opts.fetch(:version) + @uri = opts.include?(:uri) && HTTP::URI.parse(opts.fetch(:uri)) + @status = HTTP::Response::Status.new opts.fetch(:status) + @headers = HTTP::Headers.coerce(opts.fetch(:headers, {})) + + if opts.include?(:connection) + encoding = opts[:encoding] || charset || Encoding::BINARY + @body = Response::Body.new(opts.fetch(:connection), encoding) + else + @body = opts.fetch(:body) + end end # @!method reason @@ -70,19 +85,15 @@ def content_type @content_type ||= ContentType.parse headers[Headers::CONTENT_TYPE] end - # MIME type of response (if any) - # - # @return [String, nil] - def mime_type - @mime_type ||= content_type.mime_type - end + # @!method mime_type + # MIME type of response (if any) + # @return [String, nil] + def_delegator :content_type, :mime_type - # Charset of response (if any) - # - # @return [String, nil] - def charset - @charset ||= content_type.charset - end + # @!method charset + # Charset of response (if any) + # @return [String, nil] + def_delegator :content_type, :charset def cookies @cookies ||= headers.each_with_object CookieJar.new do |(k, v), jar| diff --git a/lib/http/response/body.rb b/lib/http/response/body.rb index 259ff61e..d3fcf1c9 100644 --- a/lib/http/response/body.rb +++ b/lib/http/response/body.rb @@ -9,10 +9,11 @@ class Body include Enumerable def_delegator :to_s, :empty? - def initialize(client) - @client = client - @streaming = nil - @contents = nil + def initialize(client, encoding) + @client = client + @streaming = nil + @contents = nil + @encoding = encoding end # (see HTTP::Client#readpartial) @@ -36,9 +37,9 @@ def to_s begin @streaming = false - @contents = "".force_encoding(Encoding::UTF_8) + @contents = "".force_encoding(@encoding) while (chunk = @client.readpartial) - @contents << chunk.force_encoding(Encoding::ASCII_8BIT) + @contents << chunk.force_encoding(@encoding) end rescue @contents = nil diff --git a/spec/lib/http/client_spec.rb b/spec/lib/http/client_spec.rb index 4ea78294..080f8d73 100644 --- a/spec/lib/http/client_spec.rb +++ b/spec/lib/http/client_spec.rb @@ -26,11 +26,18 @@ def stub(stubs) end def redirect_response(location, status = 302) - HTTP::Response.new(status, "1.1", {"Location" => location}, "") + HTTP::Response.new( + :status => status, + :version => "1.1", + :headers => {"Location" => location}, + :body => "") end def simple_response(body, status = 200) - HTTP::Response.new(status, "1.1", {}, body) + HTTP::Response.new( + :status => status, + :version => "1.1", + :body => body) end describe "following redirects" do diff --git a/spec/lib/http/options/merge_spec.rb b/spec/lib/http/options/merge_spec.rb index 874779ce..538d9cfc 100644 --- a/spec/lib/http/options/merge_spec.rb +++ b/spec/lib/http/options/merge_spec.rb @@ -55,6 +55,7 @@ :socket_class => described_class.default_socket_class, :ssl_socket_class => described_class.default_ssl_socket_class, :ssl_context => nil, - :cookies => {}) + :cookies => {}, + :encoding => nil) end end diff --git a/spec/lib/http/redirector_spec.rb b/spec/lib/http/redirector_spec.rb index c71ecf15..4ee0e86f 100644 --- a/spec/lib/http/redirector_spec.rb +++ b/spec/lib/http/redirector_spec.rb @@ -1,6 +1,11 @@ RSpec.describe HTTP::Redirector do def simple_response(status, body = "", headers = {}) - HTTP::Response.new(status, "1.1", headers, body) + HTTP::Response.new( + :status => status, + :version => "1.1", + :headers => headers, + :body => body + ) end def redirect_response(status, location) diff --git a/spec/lib/http/response/body_spec.rb b/spec/lib/http/response/body_spec.rb index 4dec4312..6e1488d0 100644 --- a/spec/lib/http/response/body_spec.rb +++ b/spec/lib/http/response/body_spec.rb @@ -4,7 +4,7 @@ before { allow(client).to receive(:readpartial) { chunks.shift } } - subject(:body) { described_class.new client } + subject(:body) { described_class.new client, Encoding::UTF_8 } it "streams bodies from responses" do expect(subject.to_s).to eq "Hello, World!" diff --git a/spec/lib/http/response_spec.rb b/spec/lib/http/response_spec.rb index 87ce6660..bd797156 100644 --- a/spec/lib/http/response_spec.rb +++ b/spec/lib/http/response_spec.rb @@ -2,7 +2,16 @@ let(:body) { "Hello world!" } let(:uri) { "http://example.com/" } let(:headers) { {} } - subject(:response) { HTTP::Response.new 200, "1.1", headers, body, uri } + + subject(:response) do + HTTP::Response.new( + :status => 200, + :version => "1.1", + :headers => headers, + :body => body, + :uri => uri + ) + end it "includes HTTP::Headers::Mixin" do expect(described_class).to include HTTP::Headers::Mixin diff --git a/spec/lib/http_spec.rb b/spec/lib/http_spec.rb index 258a8287..8d5c1380 100644 --- a/spec/lib/http_spec.rb +++ b/spec/lib/http_spec.rb @@ -1,3 +1,5 @@ +# encoding: UTF-8 + require "json" require "support/dummy_server" @@ -160,14 +162,36 @@ context "loading binary data" do it "is encoded as bytes" do response = HTTP.get "#{dummy.endpoint}/bytes" - expect(response.to_s.encoding).to eq(Encoding::ASCII_8BIT) + expect(response.to_s.encoding).to eq(Encoding::BINARY) + end + end + + context "loading endpoint with charset" do + it "uses charset from headers" do + response = HTTP.get "#{dummy.endpoint}/iso-8859-1" + expect(response.to_s.encoding).to eq(Encoding::ISO8859_1) + expect(response.to_s.encode(Encoding::UTF_8)).to eq("testæ") + end + + context "with encoding option" do + it "respects option" do + response = HTTP.get "#{dummy.endpoint}/iso-8859-1", "encoding" => Encoding::BINARY + expect(response.to_s.encoding).to eq(Encoding::BINARY) + end + end + end + + context "passing a string encoding type" do + it "finds encoding" do + response = HTTP.get dummy.endpoint, "encoding" => "ascii" + expect(response.to_s.encoding).to eq(Encoding::ASCII) end end - context "loading text" do - it "is utf-8 encoded" do + context "loading text with no charset" do + it "is binary encoded" do response = HTTP.get dummy.endpoint - expect(response.to_s.encoding).to eq(Encoding::UTF_8) + expect(response.to_s.encoding).to eq(Encoding::BINARY) end end diff --git a/spec/support/dummy_server/servlet.rb b/spec/support/dummy_server/servlet.rb index 47ce4d81..55fb30f1 100644 --- a/spec/support/dummy_server/servlet.rb +++ b/spec/support/dummy_server/servlet.rb @@ -1,3 +1,5 @@ +# encoding: UTF-8 + class DummyServer < WEBrick::HTTPServer class Servlet < WEBrick::HTTPServlet::AbstractServlet def self.sockets @@ -129,6 +131,11 @@ def do_#{method.upcase}(req, res) res.body = bytes.pack("c*") end + get "/iso-8859-1" do |_req, res| + res["Content-Type"] = "text/plain; charset=ISO-8859-1" + res.body = "testæ".encode(Encoding::ISO8859_1) + end + get "/cookies" do |req, res| res["Set-Cookie"] = "foo=bar" res.body = req.cookies.map { |c| [c.name, c.value].join ": " }.join("\n")