diff --git a/http.gemspec b/http.gemspec index 7cabad52..2c2c79b4 100644 --- a/http.gemspec +++ b/http.gemspec @@ -24,6 +24,7 @@ Gem::Specification.new do |gem| gem.version = HTTP::VERSION gem.add_runtime_dependency 'http_parser.rb', '~> 0.6.0' + gem.add_runtime_dependency 'form_data', '~> 0.0.1' gem.add_development_dependency 'bundler', '~> 1.0' end diff --git a/lib/http/client.rb b/lib/http/client.rb index 7c9a7740..1390ceda 100644 --- a/lib/http/client.rb +++ b/lib/http/client.rb @@ -1,5 +1,6 @@ require 'cgi' require 'uri' +require 'form_data' require 'http/options' require 'http/redirector' @@ -127,12 +128,15 @@ def normalize_uri(uri) # Create the request body object to send def make_request_body(opts, headers) - if opts.body + case + when opts.body opts.body - elsif opts.form - headers['Content-Type'] ||= 'application/x-www-form-urlencoded' - URI.encode_www_form(opts.form) - elsif opts.json + when opts.form + form = FormData.create opts.form + headers['Content-Type'] ||= form.content_type + headers['Content-Length'] ||= form.content_length + form.to_s + when opts.json headers['Content-Type'] ||= 'application/json' MimeType[:json].encode opts.json end diff --git a/lib/http/request/writer.rb b/lib/http/request/writer.rb index f4044ce9..7586cf3a 100644 --- a/lib/http/request/writer.rb +++ b/lib/http/request/writer.rb @@ -34,13 +34,8 @@ def stream def add_body_type_headers if @body.is_a?(String) && !@headers['Content-Length'] @request_header << "Content-Length: #{@body.bytesize}" - elsif @body.is_a?(Enumerable) - encoding = @headers['Transfer-Encoding'] - if encoding == 'chunked' - @request_header << 'Transfer-Encoding: chunked' - else - fail(RequestError, 'invalid transfer encoding') - end + elsif @body.is_a?(Enumerable) && 'chunked' != @headers['Transfer-Encoding'] + fail(RequestError, 'invalid transfer encoding') end end diff --git a/spec/http/request/writer_spec.rb b/spec/http/request/writer_spec.rb index 0481c078..b948b6df 100644 --- a/spec/http/request/writer_spec.rb +++ b/spec/http/request/writer_spec.rb @@ -3,41 +3,89 @@ require 'spec_helper' RSpec.describe HTTP::Request::Writer do + let(:io) { StringIO.new } + let(:body) { '' } + let(:headers) { HTTP::Headers.new } + let(:headerstart) { 'GET /test HTTP/1.1' } + + subject(:writer) { described_class.new(io, body, headers, headerstart) } + describe '#initalize' do - def construct(body) - HTTP::Request::Writer.new(nil, body, [], '') - end + context 'when body is nil' do + let(:body) { nil } - it "doesn't throw on a nil body" do - expect { construct nil }.not_to raise_error + it 'does not raise an error' do + expect { writer }.not_to raise_error + end end - it "doesn't throw on a String body" do - expect { construct 'string body' }.not_to raise_error - end + context 'when body is a string' do + let(:body) { 'string body' } - it "doesn't throw on an Enumerable body" do - expect { construct %w(bees cows) }.not_to raise_error + it 'does not raise an error' do + expect { writer }.not_to raise_error + end end - it "does throw on a body that isn't string, enumerable or nil" do - expect { construct true }.to raise_error + context 'when body is an Enumerable' do + let(:body) { %w(bees cows) } + + it 'does not raise an error' do + expect { writer }.not_to raise_error + end end - it 'writes a chunked request from an Enumerable correctly' do - io = StringIO.new - writer = HTTP::Request::Writer.new(io, %w(bees cows), [], '') - writer.send_request_body - io.rewind - expect(io.string).to eq "4\r\nbees\r\n4\r\ncows\r\n0\r\n\r\n" + context 'when body is not string, enumerable or nil' do + let(:body) { 123 } + + it 'raises an error' do + expect { writer }.to raise_error + end end end - describe '#add_body_type_headers' do - it 'properly calculates length of unicode string' do - writer = HTTP::Request::Writer.new(nil, 'Привет, мир!', {}, '') - writer.add_body_type_headers - expect(writer.join_headers).to match(/\r\nContent-Length: 21\r\n/) + describe '#stream' do + context 'when body is Enumerable' do + let(:body) { %w(bees cows) } + let(:headers) { HTTP::Headers.coerce 'Transfer-Encoding' => 'chunked' } + + it 'writes a chunked request from an Enumerable correctly' do + writer.stream + expect(io.string).to end_with "\r\n4\r\nbees\r\n4\r\ncows\r\n0\r\n\r\n" + end + + it 'writes Transfer-Encoding header only once' do + writer.stream + expect(io.string).to start_with "#{headerstart}\r\nTransfer-Encoding: chunked\r\n\r\n" + end + + context 'when Transfer-Encoding not set' do + let(:headers) { HTTP::Headers.new } + specify { expect { writer.stream }.to raise_error } + end + + context 'when Transfer-Encoding is not chunked' do + let(:headers) { HTTP::Headers.coerce 'Transfer-Encoding' => 'gzip' } + specify { expect { writer.stream }.to raise_error } + end + end + + context 'when body is a unicode String' do + let(:body) { 'Привет, мир!' } + + it 'properly calculates Content-Length if needed' do + writer.stream + expect(io.string).to start_with "#{headerstart}\r\nContent-Length: 21\r\n\r\n" + end + + context 'when Content-Length explicitly set' do + let(:headers) { HTTP::Headers.coerce 'Content-Length' => 12 } + + it 'keeps given value' do + writer.stream + expect(io.string).to start_with "#{headerstart}\r\nContent-Length: 12\r\n\r\n" + end + end end end end