diff --git a/Gemfile b/Gemfile index d2e6dfd37..0ffb9c69b 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,6 @@ gem 'jruby-openssl', '~> 0.11.0', platforms: :jruby group :development, :test do gem 'coveralls_reborn', require: false - gem 'multipart-parser' gem 'pry' gem 'rack', '~> 2.2' gem 'rake' diff --git a/UPGRADING.md b/UPGRADING.md index 6c816373a..98dfc1981 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -58,6 +58,34 @@ So don't fret yet! We're doing our best to support our `faraday_middleware` user It will obviously take time for some of the middleware in `faraday_middleware` to make its way into a separate gem, so we appreciate this might be an upgrade obstacle for some. However this is part of an effort to focus the core team resources tackling the most requested features. We'll be listening to the community and prioritize middleware that are most used, and will be welcoming contributors who want to become owners of the middleware when these become separate from the `faraday_middleware` repo. +### Bundled middleware moved out + +Moving middleware into its own gem makes sense not only for `faraday_middleware`, but also for middleware bundled with Faraday. +As of v2.0, the `retry` and `multipart` middleware have been moved to separate `faraday-retry` and `faraday-multipart` gems. +These have been identified as good candidates due to their complexity and external dependencies. +Thanks to this change, we were able to make Faraday 2.0 completely dependency free ๐ŸŽ‰ (the only exception being `ruby2_keywords`, which will be necessary only while we keep supporting Ruby 2.6). + +#### So what should I do if I currently use the `retry` or `multipart` middleware? + +Upgrading is pretty simple, because the middleware was simply moved out to external gems. +All you need to do is to add them to your gemfile (either `faraday-retry` or `faraday-multipart` and require them before usage: + +```ruby +# Gemfile +gem 'faraday-multipart' +gem 'faraday-retry' + +# Connection initializer +require 'faraday/retry' +require 'faraday/multipart + +conn = Faraday.new(url) do |f| + f.request :multipart + f.request :retry + # ... +end +``` + ### Autoloading and dependencies Faraday has until now provided and relied on a complex dynamic dependencies system. @@ -84,9 +112,7 @@ For more details, see https://github.com/lostisland/faraday/pull/1306 * Rename `Faraday::Request#method` to `#http_method`. * Remove `Faraday::Response::Middleware`. You can now use the new `on_complete` callback provided by `Faraday::Middleware`. -* Drop `Faraday::UploadIO` in favour of `Faraday::FilePart`. * `Faraday.default_connection_options` will now be deep-merged into new connections to avoid overriding them (e.g. headers). -* Retry middleware has been moved to a separate `faraday-retry` gem. * `Faraday::Builder#build` method is not exposed through `Faraday::Connection` anymore and does not reset the handlers if called multiple times. This method should be used internally only. ## Faraday 1.0 diff --git a/docs/middleware/index.md b/docs/middleware/index.md index 98a66995c..30ebb0db7 100644 --- a/docs/middleware/index.md +++ b/docs/middleware/index.md @@ -106,32 +106,23 @@ Here's a more realistic example: ```ruby Faraday.new(...) do |conn| - # POST/PUT params encoders: - conn.request :multipart + # POST/PUT params encoder conn.request :url_encoded - # Last middleware must be the adapter: + # Logging of requests/responses + conn.response :logger + + # Last middleware must be the adapter conn.adapter :typhoeus end ``` This request middleware setup affects POST/PUT requests in the following way: -1. `Request::Multipart` checks for files in the payload, otherwise leaves - everything untouched; -2. `Request::UrlEncoded` encodes as "application/x-www-form-urlencoded" if not - already encoded or of another type +1. `Request::UrlEncoded` encodes as "application/x-www-form-urlencoded" if not + already encoded or of another type. +2. `Response::Logger' logs request and response headers, can be configured to log bodies as well. Swapping middleware means giving the other priority. Specifying the "Content-Type" for the request is explicitly stating which middleware should process it. - -For example: - -```ruby -# uploading a file: -payload[:profile_pic] = Faraday::FilePart.new('/path/to/avatar.jpg', 'image/jpeg') - -# "Multipart" middleware detects files and encodes with "multipart/form-data": -conn.put '/profile', payload -``` diff --git a/docs/middleware/list.md b/docs/middleware/list.md index 467f6d0e8..6d5d86f94 100644 --- a/docs/middleware/list.md +++ b/docs/middleware/list.md @@ -24,8 +24,6 @@ content type. * [`BasicAuthentication`][authentication] sets the `Authorization` header to the `user:password` base64 representation. * [`TokenAuthentication`][authentication] sets the `Authorization` header to the specified token. -* [`Multipart`][multipart] converts a `Faraday::Request#body` hash of key/value pairs into a -multipart form request. * [`UrlEncoded`][url_encoded] converts a `Faraday::Request#body` hash of key/value pairs into a url-encoded request body. * [`Json Request`][json-request] converts a `Faraday::Request#body` hash of key/value pairs into a JSON request body. * [`Json Response`][json-response] parses response body into a hash of key/value pairs. @@ -42,7 +40,6 @@ before returning it. [authentication]: ./authentication -[multipart]: ./multipart [url_encoded]: ./url-encoded [json-request]: ./json-request [instrumentation]: ./instrumentation diff --git a/docs/middleware/request/authentication.md b/docs/middleware/request/authentication.md index 4dc5ab486..f365552f2 100644 --- a/docs/middleware/request/authentication.md +++ b/docs/middleware/request/authentication.md @@ -3,8 +3,8 @@ layout: documentation title: "Authentication Middleware" permalink: /middleware/authentication hide: true -next_name: Multipart Middleware -next_link: ./multipart +next_name: UrlEncoded Middleware +next_link: ./url-encoded top_name: Back to Middleware top_link: ./list --- diff --git a/docs/middleware/request/multipart.md b/docs/middleware/request/multipart.md deleted file mode 100644 index 7585acd1f..000000000 --- a/docs/middleware/request/multipart.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -layout: documentation -title: "Multipart Middleware" -permalink: /middleware/multipart -hide: true -prev_name: Authentication Middleware -prev_link: ./authentication -next_name: UrlEncoded Middleware -next_link: ./url-encoded -top_name: Back to Middleware -top_link: ./list ---- - -The `Multipart` middleware converts a `Faraday::Request#body` Hash of key/value -pairs into a multipart form request, but only under these conditions: - -* The request's Content-Type is "multipart/form-data" -* Content-Type is unspecified, AND one of the values in the Body responds to -`#content_type`. - -Faraday contains a couple helper classes for multipart values: - -* `Faraday::FilePart` wraps binary file data with a Content-Type. The file data -can be specified with a String path to a local file, or an IO object. -* `Faraday::ParamPart` wraps a String value with a Content-Type, and optionally -a Content-ID. - -Note: `Faraday::ParamPart` was added in Faraday v0.16.0. Before that, -`Faraday::FilePart` was called `Faraday::UploadIO`. - -### Example Usage - -```ruby -conn = Faraday.new(...) do |f| - f.request :multipart - ... -end -``` - -Payload can be a mix of POST data and multipart values. - -```ruby -# regular POST form value -payload = { string: 'value' } - -# filename for this value is File.basename(__FILE__) -payload[:file] = Faraday::FilePart.new(__FILE__, 'text/x-ruby') - -# specify filename because IO object doesn't know it -payload[:file_with_name] = Faraday::FilePart.new(File.open(__FILE__), - 'text/x-ruby', - File.basename(__FILE__)) - -# Sets a custom Content-Disposition: -# nil filename still defaults to File.basename(__FILE__) -payload[:file_with_header] = Faraday::FilePart.new(__FILE__, - 'text/x-ruby', nil, - 'Content-Disposition' => 'form-data; foo=1') - -# Upload raw json with content type -payload[:raw_data] = Faraday::ParamPart.new({a: 1}.to_json, 'application/json') - -# optionally sets Content-ID too -payload[:raw_with_id] = Faraday::ParamPart.new({a: 1}.to_json, 'application/json', - 'foo-123') - -conn.post('/', payload) -``` diff --git a/docs/middleware/request/url_encoded.md b/docs/middleware/request/url_encoded.md index c913e3865..8d15a52d2 100644 --- a/docs/middleware/request/url_encoded.md +++ b/docs/middleware/request/url_encoded.md @@ -3,8 +3,8 @@ layout: documentation title: "UrlEncoded Middleware" permalink: /middleware/url-encoded hide: true -prev_name: Multipart Middleware -prev_link: ./multipart +prev_name: Authentication Middleware +prev_link: ./authentication next_name: JSON Request Middleware next_link: ./json-request top_name: Back to Middleware diff --git a/docs/usage/index.md b/docs/usage/index.md index 53c96753f..ab82db485 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -106,7 +106,8 @@ response = conn.post('post', '{"boom": "zap"}', #### Posting Forms -Faraday will automatically convert key/value hashes into proper form bodies. +Faraday will automatically convert key/value hashes into proper form bodies +thanks to the `url_encoded` middleware included in the default connection. ```ruby # POST 'application/x-www-form-urlencoded' content @@ -114,8 +115,6 @@ response = conn.post('post', boom: 'zap') # => POST 'boom=zap' to http://httpbingo.org/post ``` -Faraday can also [upload files][multipart]. - ### Detailed HTTP Requests Faraday supports a longer style for making requests. This is handy if you need @@ -185,4 +184,3 @@ Note that if you create your own connection with middleware, it won't encode form bodies unless you too include the [`:url_encoded`](encoding) middleware! [encoding]: ../middleware/url-encoded -[multipart]: ../middleware/multipart diff --git a/faraday.gemspec b/faraday.gemspec index 11ce82812..6a078942d 100644 --- a/faraday.gemspec +++ b/faraday.gemspec @@ -15,7 +15,6 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 2.6' - spec.add_dependency 'multipart-post', '>= 1.2', '< 3' spec.add_dependency 'ruby2_keywords', '>= 0.0.4' # Includes `examples` and `spec` to allow external adapter gems to run Faraday unit and integration tests diff --git a/lib/faraday.rb b/lib/faraday.rb index 4b1354087..971006205 100644 --- a/lib/faraday.rb +++ b/lib/faraday.rb @@ -17,8 +17,6 @@ require 'faraday/adapter' require 'faraday/request' require 'faraday/response' -require 'faraday/file_part' -require 'faraday/param_part' # This is the main namespace for Faraday. # diff --git a/lib/faraday/file_part.rb b/lib/faraday/file_part.rb deleted file mode 100644 index d8c355400..000000000 --- a/lib/faraday/file_part.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -require 'stringio' - -# multipart-post gem -require 'composite_io' -require 'parts' - -module Faraday - # Multipart value used to POST a binary data from a file or - # - # @example - # payload = { file: Faraday::FilePart.new("file_name.ext", "content/type") } - # http.post("/upload", payload) - # - - # @!method initialize(filename_or_io, content_type, filename = nil, opts = {}) - # - # @param filename_or_io [String, IO] Either a String filename to a local - # file or an open IO object. - # @param content_type [String] String content type of the file data. - # @param filename [String] Optional String filename, usually to add context - # to a given IO object. - # @param opts [Hash] Optional Hash of String key/value pairs to describethis - # this uploaded file. Expected Header keys include: - # * Content-Transfer-Encoding - Defaults to "binary" - # * Content-Disposition - Defaults to "form-data" - # * Content-Type - Defaults to the content_type argument. - # * Content-ID - Optional. - # - # @return [Faraday::FilePart] - # - # @!attribute [r] content_type - # The uploaded binary data's content type. - # - # @return [String] - # - # @!attribute [r] original_filename - # The base filename, taken either from the filename_or_io or filename - # arguments in #initialize. - # - # @return [String] - # - # @!attribute [r] opts - # Extra String key/value pairs to make up the header for this uploaded file. - # - # @return [Hash] - # - # @!attribute [r] io - # The open IO object for the uploaded file. - # - # @return [IO] - FilePart = ::UploadIO - - Parts = ::Parts - - # Similar to, but not compatible with CompositeReadIO provided by the - # multipart-post gem. - # https://github.com/nicksieger/multipart-post/blob/master/lib/composite_io.rb - class CompositeReadIO - def initialize(*parts) - @parts = parts.flatten - @ios = @parts.map(&:to_io) - @index = 0 - end - - # @return [Integer] sum of the lengths of all the parts - def length - @parts.inject(0) { |sum, part| sum + part.length } - end - - # Rewind each of the IOs and reset the index to 0. - # - # @return [void] - def rewind - @ios.each(&:rewind) - @index = 0 - end - - # Read from IOs in order until `length` bytes have been received. - # - # @param length [Integer, nil] - # @param outbuf [String, nil] - def read(length = nil, outbuf = nil) - got_result = false - outbuf = outbuf ? (+outbuf).replace('') : +'' - - while (io = current_io) - if (result = io.read(length)) - got_result ||= !result.nil? - result.force_encoding('BINARY') if result.respond_to?(:force_encoding) - outbuf << result - length -= result.length if length - break if length&.zero? - end - advance_io - end - !got_result && length ? nil : outbuf - end - - # Close each of the IOs. - # - # @return [void] - def close - @ios.each(&:close) - end - - def ensure_open_and_readable - # Rubinius compatibility - end - - private - - def current_io - @ios[@index] - end - - def advance_io - @index += 1 - end - end -end diff --git a/lib/faraday/param_part.rb b/lib/faraday/param_part.rb deleted file mode 100644 index c1279c359..000000000 --- a/lib/faraday/param_part.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Faraday - # Multipart value used to POST data with a content type. - class ParamPart - # @param value [String] Uploaded content as a String. - # @param content_type [String] String content type of the value. - # @param content_id [String] Optional String of this value's Content-ID. - # - # @return [Faraday::ParamPart] - def initialize(value, content_type, content_id = nil) - @value = value - @content_type = content_type - @content_id = content_id - end - - # Converts this value to a form part. - # - # @param boundary [String] String multipart boundary that must not exist in - # the content exactly. - # @param key [String] String key name for this value. - # - # @return [Faraday::Parts::Part] - def to_part(boundary, key) - Faraday::Parts::Part.new(boundary, key, value, headers) - end - - # Returns a Hash of String key/value pairs. - # - # @return [Hash] - def headers - { - 'Content-Type' => content_type, - 'Content-ID' => content_id - } - end - - # The content to upload. - # - # @return [String] - attr_reader :value - - # The value's content type. - # - # @return [String] - attr_reader :content_type - - # The value's content ID, if given. - # - # @return [String, nil] - attr_reader :content_id - end -end diff --git a/lib/faraday/request.rb b/lib/faraday/request.rb index b213e0e8d..d87639a0c 100644 --- a/lib/faraday/request.rb +++ b/lib/faraday/request.rb @@ -133,5 +133,4 @@ def to_env(connection) require 'faraday/request/authorization' require 'faraday/request/instrumentation' require 'faraday/request/json' -require 'faraday/request/multipart' require 'faraday/request/url_encoded' diff --git a/lib/faraday/request/multipart.rb b/lib/faraday/request/multipart.rb deleted file mode 100644 index 64b2f2526..000000000 --- a/lib/faraday/request/multipart.rb +++ /dev/null @@ -1,108 +0,0 @@ -# frozen_string_literal: true - -require File.expand_path('url_encoded', __dir__) -require 'securerandom' - -module Faraday - class Request - # Middleware for supporting multi-part requests. - class Multipart < UrlEncoded - self.mime_type = 'multipart/form-data' - unless defined?(::Faraday::Request::Multipart::DEFAULT_BOUNDARY_PREFIX) - DEFAULT_BOUNDARY_PREFIX = '-----------RubyMultipartPost' - end - - def initialize(app = nil, options = {}) - super(app) - @options = options - end - - # Checks for files in the payload, otherwise leaves everything untouched. - # - # @param env [Faraday::Env] - def call(env) - match_content_type(env) do |params| - env.request.boundary ||= unique_boundary - env.request_headers[CONTENT_TYPE] += - "; boundary=#{env.request.boundary}" - env.body = create_multipart(env, params) - end - @app.call env - end - - # @param env [Faraday::Env] - def process_request?(env) - type = request_type(env) - env.body.respond_to?(:each_key) && !env.body.empty? && ( - (type.empty? && has_multipart?(env.body)) || - (type == self.class.mime_type) - ) - end - - # Returns true if obj is an enumerable with values that are multipart. - # - # @param obj [Object] - # @return [Boolean] - def has_multipart?(obj) # rubocop:disable Naming/PredicateName - if obj.respond_to?(:each) - (obj.respond_to?(:values) ? obj.values : obj).each do |val| - return true if val.respond_to?(:content_type) || has_multipart?(val) - end - end - false - end - - # @param env [Faraday::Env] - # @param params [Hash] - def create_multipart(env, params) - boundary = env.request.boundary - parts = process_params(params) do |key, value| - part(boundary, key, value) - end - parts << Faraday::Parts::EpiloguePart.new(boundary) - - body = Faraday::CompositeReadIO.new(parts) - env.request_headers[Faraday::Env::ContentLength] = body.length.to_s - body - end - - def part(boundary, key, value) - if value.respond_to?(:to_part) - value.to_part(boundary, key) - else - Faraday::Parts::Part.new(boundary, key, value) - end - end - - # @return [String] - def unique_boundary - "#{DEFAULT_BOUNDARY_PREFIX}-#{SecureRandom.hex}" - end - - # @param params [Hash] - # @param prefix [String] - # @param pieces [Array] - def process_params(params, prefix = nil, pieces = nil, &block) - params.inject(pieces || []) do |all, (key, value)| - if prefix - key = @options[:flat_encode] ? prefix.to_s : "#{prefix}[#{key}]" - end - - case value - when Array - values = value.inject([]) { |a, v| a << [nil, v] } - process_params(values, key, all, &block) - when Hash - process_params(value, key, all, &block) - else - # rubocop:disable Performance/RedundantBlockCall - all << block.call(key, value) - # rubocop:enable Performance/RedundantBlockCall - end - end - end - end - end -end - -Faraday::Request.register_middleware(multipart: Faraday::Request::Multipart) diff --git a/spec/faraday/composite_read_io_spec.rb b/spec/faraday/composite_read_io_spec.rb deleted file mode 100644 index ccba34f3d..000000000 --- a/spec/faraday/composite_read_io_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -require 'stringio' - -RSpec.describe Faraday::CompositeReadIO do - Part = Struct.new(:to_io) do - def length - to_io.string.length - end - end - - def part(str) - Part.new StringIO.new(str) - end - - def composite_io(*parts) - Faraday::CompositeReadIO.new(*parts) - end - - context 'with empty composite_io' do - subject { composite_io } - - it { expect(subject.length).to eq(0) } - it { expect(subject.read).to eq('') } - it { expect(subject.read(1)).to be_nil } - end - - context 'with empty parts' do - subject { composite_io(part(''), part('')) } - - it { expect(subject.length).to eq(0) } - it { expect(subject.read).to eq('') } - it { expect(subject.read(1)).to be_nil } - end - - context 'with 2 parts' do - subject { composite_io(part('abcd'), part('1234')) } - - it { expect(subject.length).to eq(8) } - it { expect(subject.read).to eq('abcd1234') } - it 'allows to read in chunks' do - expect(subject.read(3)).to eq('abc') - expect(subject.read(3)).to eq('d12') - expect(subject.read(3)).to eq('34') - expect(subject.read(3)).to be_nil - end - it 'allows to rewind while reading in chunks' do - expect(subject.read(3)).to eq('abc') - expect(subject.read(3)).to eq('d12') - subject.rewind - expect(subject.read(3)).to eq('abc') - expect(subject.read(5)).to eq('d1234') - expect(subject.read(3)).to be_nil - subject.rewind - expect(subject.read(2)).to eq('ab') - end - end - - context 'with mix of empty and non-empty parts' do - subject { composite_io(part(''), part('abcd'), part(''), part('1234'), part('')) } - - it 'allows to read in chunks' do - expect(subject.read(6)).to eq('abcd12') - expect(subject.read(6)).to eq('34') - expect(subject.read(6)).to be_nil - end - end - - context 'with utf8 multibyte part' do - subject { composite_io(part("\x86"), part('ใƒ•ใ‚กใ‚คใƒซ')) } - - it { expect(subject.read).to eq(String.new("\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB", encoding: 'BINARY')) } - it 'allows to read in chunks' do - expect(subject.read(3)).to eq(String.new("\x86\xE3\x83", encoding: 'BINARY')) - expect(subject.read(3)).to eq(String.new("\x95\xE3\x82", encoding: 'BINARY')) - expect(subject.read(8)).to eq(String.new("\xA1\xE3\x82\xA4\xE3\x83\xAB", encoding: 'BINARY')) - expect(subject.read(3)).to be_nil - end - end -end diff --git a/spec/faraday/request/multipart_spec.rb b/spec/faraday/request/multipart_spec.rb deleted file mode 100644 index 5a6e2e14b..000000000 --- a/spec/faraday/request/multipart_spec.rb +++ /dev/null @@ -1,302 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Faraday::Request::Multipart do - let(:options) { {} } - let(:conn) do - Faraday.new do |b| - b.request :multipart, options - b.request :url_encoded - b.adapter :test do |stub| - stub.post('/echo') do |env| - posted_as = env[:request_headers]['Content-Type'] - expect(env[:body]).to be_a_kind_of(Faraday::CompositeReadIO) - [200, { 'Content-Type' => posted_as }, env[:body].read] - end - end - end - end - - shared_examples 'a multipart request' do - it 'generates a unique boundary for each request' do - response1 = conn.post('/echo', payload) - response2 = conn.post('/echo', payload) - - b1 = parse_multipart_boundary(response1.headers['Content-Type']) - b2 = parse_multipart_boundary(response2.headers['Content-Type']) - expect(b1).to_not eq(b2) - end - end - - context 'FilePart: when multipart objects in param' do - let(:payload) do - { - a: 1, - b: { - c: Faraday::FilePart.new(__FILE__, 'text/x-ruby', nil, - 'Content-Disposition' => 'form-data; foo=1'), - d: 2 - } - } - end - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']) - .to eq( - 'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end - - context 'FilePart: when providing json and IO content in the same payload' do - let(:io) { StringIO.new('io-content') } - let(:json) do - { - b: 1, - c: 2 - }.to_json - end - - let(:payload) do - { - json: Faraday::ParamPart.new(json, 'application/json'), - io: Faraday::FilePart.new(io, 'application/pdf') - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_json, body_json = result.part('json') - expect(part_json).to_not be_nil - expect(part_json.mime).to eq('application/json') - expect(part_json.filename).to be_nil - expect(body_json).to eq(json) - - part_io, body_io = result.part('io') - expect(part_io).to_not be_nil - expect(part_io.mime).to eq('application/pdf') - expect(part_io.filename).to eq('local.path') - expect(body_io).to eq(io.string) - end - end - - context 'FilePart: when multipart objects in array param' do - let(:payload) do - { - a: 1, - b: [{ - c: Faraday::FilePart.new(__FILE__, 'text/x-ruby'), - d: 2 - }] - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[][c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']) - .to eq( - 'form-data; name="b[][c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[][d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end - - context 'UploadIO: when multipart objects in param' do - let(:payload) do - { - a: 1, - b: { - c: Faraday::FilePart.new(__FILE__, 'text/x-ruby', nil, - 'Content-Disposition' => 'form-data; foo=1'), - d: 2 - } - } - end - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']) - .to eq( - 'form-data; foo=1; name="b[c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end - - context 'UploadIO: when providing json and IO content in the same payload' do - let(:io) { StringIO.new('io-content') } - let(:json) do - { - b: 1, - c: 2 - }.to_json - end - - let(:payload) do - { - json: Faraday::ParamPart.new(json, 'application/json'), - io: Faraday::FilePart.new(io, 'application/pdf') - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_json, body_json = result.part('json') - expect(part_json).to_not be_nil - expect(part_json.mime).to eq('application/json') - expect(part_json.filename).to be_nil - expect(body_json).to eq(json) - - part_io, body_io = result.part('io') - expect(part_io).to_not be_nil - expect(part_io.mime).to eq('application/pdf') - expect(part_io.filename).to eq('local.path') - expect(body_io).to eq(io.string) - end - end - - context 'UploadIO: when multipart objects in array param' do - let(:payload) do - { - a: 1, - b: [{ - c: Faraday::FilePart.new(__FILE__, 'text/x-ruby'), - d: 2 - }] - } - end - - it_behaves_like 'a multipart request' - - it 'forms a multipart request' do - response = conn.post('/echo', payload) - - boundary = parse_multipart_boundary(response.headers['Content-Type']) - result = parse_multipart(boundary, response.body) - expect(result[:errors]).to be_empty - - part_a, body_a = result.part('a') - expect(part_a).to_not be_nil - expect(part_a.filename).to be_nil - expect(body_a).to eq('1') - - part_bc, body_bc = result.part('b[][c]') - expect(part_bc).to_not be_nil - expect(part_bc.filename).to eq('multipart_spec.rb') - expect(part_bc.headers['content-disposition']) - .to eq( - 'form-data; name="b[][c]"; filename="multipart_spec.rb"' - ) - expect(part_bc.headers['content-type']).to eq('text/x-ruby') - expect(part_bc.headers['content-transfer-encoding']).to eq('binary') - expect(body_bc).to eq(File.read(__FILE__)) - - part_bd, body_bd = result.part('b[][d]') - expect(part_bd).to_not be_nil - expect(part_bd.filename).to be_nil - expect(body_bd).to eq('2') - end - end - - context 'when passing flat_encode=true option' do - let(:options) { { flat_encode: true } } - let(:io) { StringIO.new('io-content') } - let(:payload) do - { - a: 1, - b: [ - Faraday::FilePart.new(io, 'application/pdf'), - Faraday::FilePart.new(io, 'application/pdf') - ] - } - end - - it_behaves_like 'a multipart request' - - it 'encode params using flat encoder' do - response = conn.post('/echo', payload) - - expect(response.body).to include('name="b"') - expect(response.body).not_to include('name="b[]"') - end - end -end diff --git a/spec/faraday/request/url_encoded_spec.rb b/spec/faraday/request/url_encoded_spec.rb index 9f89a5615..be377cdb1 100644 --- a/spec/faraday/request/url_encoded_spec.rb +++ b/spec/faraday/request/url_encoded_spec.rb @@ -3,7 +3,6 @@ RSpec.describe Faraday::Request::UrlEncoded do let(:conn) do Faraday.new do |b| - b.request :multipart b.request :url_encoded b.adapter :test do |stub| stub.post('/echo') do |env| diff --git a/spec/support/helper_methods.rb b/spec/support/helper_methods.rb index d63bd59f9..0f5d4f5a5 100644 --- a/spec/support/helper_methods.rb +++ b/spec/support/helper_methods.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'multipart_parser/reader' - module Faraday module HelperMethods def self.included(base) @@ -86,41 +84,6 @@ def capture_warnings end end - def multipart_file - Faraday::FilePart.new(__FILE__, 'text/x-ruby') - end - - # parse boundary out of a Content-Type header like: - # Content-Type: multipart/form-data; boundary=gc0p4Jq0M2Yt08jU534c0p - def parse_multipart_boundary(ctype) - MultipartParser::Reader.extract_boundary_value(ctype) - end - - # parse a multipart MIME message, returning a hash of any multipart errors - def parse_multipart(boundary, body) - reader = MultipartParser::Reader.new(boundary) - result = { errors: [], parts: [] } - def result.part(name) - hash = self[:parts].detect { |h| h[:part].name == name } - [hash[:part], hash[:body].join] - end - - reader.on_part do |part| - result[:parts] << thispart = { - part: part, - body: [] - } - part.on_data do |chunk| - thispart[:body] << chunk - end - end - reader.on_error do |msg| - result[:errors] << msg - end - reader.write(body) - result - end - def method_with_body?(method) self.class.method_with_body?(method) end diff --git a/spec/support/shared_examples/adapter.rb b/spec/support/shared_examples/adapter.rb index 47c0d2432..6487367d9 100644 --- a/spec/support/shared_examples/adapter.rb +++ b/spec/support/shared_examples/adapter.rb @@ -40,7 +40,6 @@ conn_options[:ssl][:ca_file] ||= ENV['SSL_FILE'] Faraday.new(remote, conn_options) do |conn| - conn.request :multipart conn.request :url_encoded conn.response :raise_error conn.adapter described_class, *adapter_options diff --git a/spec/support/shared_examples/request_method.rb b/spec/support/shared_examples/request_method.rb index 6d974f9fc..b6336549a 100644 --- a/spec/support/shared_examples/request_method.rb +++ b/spec/support/shared_examples/request_method.rb @@ -126,19 +126,6 @@ expect { conn.public_send(http_method, '/') }.to raise_error(exc) end - # Can't send files on get, head and delete methods - if method_with_body?(http_method) - it 'sends files' do - payload = { uploaded_file: multipart_file } - request_stub.with(headers: { 'Content-Type' => %r{\Amultipart/form-data} }) do |request| - # WebMock does not support matching body for multipart/form-data requests yet :( - # https://github.com/bblimke/webmock/issues/623 - request.body.include?('RubyMultipartPost') - end - conn.public_send(http_method, '/', payload) - end - end - on_feature :reason_phrase_parse do it 'parses the reason phrase' do request_stub.to_return(status: [200, 'OK'])