From 4f68590ab581b302bf0a2cd5662e8e30bd1ae92e Mon Sep 17 00:00:00 2001 From: Juna Ootoovak <135796148+juna-nb@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:48:57 -0800 Subject: [PATCH] Add The Ability To Specify A Different MIME Type (#14) There are many different mime types for multipart requests and this change provides the option to specify a different one. If a multipart mime type is not specified the middleware will default to multipart/form-data. This change also changes the structure of the middleware a bit. It changes the parent from UrlEncoded to inherit from Middleware directly instead. This is because we were overriding most of the UrlEncoded methods anyway so bringing last remaining methods over and removing that link in the dependency chain seemed to make sense. Another structural code change was to move the methods that didn't need to be public under the private keyword. The intent of this was to help future developers know what they can change without worrying breaking any external dependencies. --- README.md | 12 ++++++ lib/faraday/multipart/middleware.rb | 40 ++++++++++++++++++-- spec/faraday/multipart/middleware_spec.rb | 45 +++++++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0b6fcb5..81492a5 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,18 @@ conn = Faraday.new(...) do |f| end ``` +If you need to [specify a different content type for the multipart +request](https://www.iana.org/assignments/media-types/media-types.xhtml#multipart), +you can do so by providing the `content_type` option but it must start with +`multipart/` +otherwise it will default to `multipart/form-data`: + +```ruby +conn = Faraday.new(...) do |f| + f.request :multipart, content_type: 'multipart/mixed' + # ... +end +``` Payload can be a mix of POST data and multipart values. diff --git a/lib/faraday/multipart/middleware.rb b/lib/faraday/multipart/middleware.rb index 4084142..a764b71 100644 --- a/lib/faraday/multipart/middleware.rb +++ b/lib/faraday/multipart/middleware.rb @@ -5,11 +5,10 @@ module Faraday module Multipart # Middleware for supporting multi-part requests. - class Middleware < Faraday::Request::UrlEncoded + class Middleware < Faraday::Middleware + CONTENT_TYPE = 'Content-Type' DEFAULT_BOUNDARY_PREFIX = '-----------RubyMultipartPost' - self.mime_type = 'multipart/form-data' - def initialize(app = nil, options = {}) super(app) @options = options @@ -28,15 +27,37 @@ def call(env) @app.call env end + private + + # @param env [Faraday::Env] + # @yield [request_body] Body of the request + def match_content_type(env) + return unless process_request?(env) + + env.request_headers[CONTENT_TYPE] ||= mime_type + return if env.body.respond_to?(:to_str) || env.body.respond_to?(:read) + + yield(env.body) + 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) + (type == mime_type) ) end + # @param env [Faraday::Env] + # + # @return [String] + def request_type(env) + type = env.request_headers[CONTENT_TYPE].to_s + type = type.split(';', 2).first if type.index(';') + type + end + # Returns true if obj is an enumerable with values that are multipart. # # @param obj [Object] @@ -97,6 +118,17 @@ def process_params(params, prefix = nil, pieces = nil, &block) end end end + + # Determines and provides the multipart mime type for the request. + # + # @return [String] the multipart mime type + def mime_type + @mime_type ||= if @options[:content_type].to_s.match?(%r{\Amultipart/.+}) + @options[:content_type].to_s + else + 'multipart/form-data' + end + end end end end diff --git a/spec/faraday/multipart/middleware_spec.rb b/spec/faraday/multipart/middleware_spec.rb index 4293c1a..3b3055a 100644 --- a/spec/faraday/multipart/middleware_spec.rb +++ b/spec/faraday/multipart/middleware_spec.rb @@ -315,4 +315,49 @@ expect(response.body).not_to include('name="b[]"') end end + + context 'when passing content_type option' do + let(:payload) do + { + xml: Faraday::Multipart::ParamPart.new('', 'text/xml'), + io: Faraday::Multipart::FilePart.new(StringIO.new('io-content'), 'application/octet-stream') + } + end + + context 'when a multipart mime type is provided' do + let(:options) { { content_type: 'multipart/mixed' } } + + it_behaves_like 'a multipart request' + + it 'uses the provided mime type' do + response = conn.post('/echo', payload) + + expect(response.headers['Content-Type']).to start_with('multipart/mixed') + end + end + + context 'when a non-multipart mime type is provided' do + let(:options) { { content_type: 'application/json' } } + + it_behaves_like 'a multipart request' + + it 'uses the default mime type' do + response = conn.post('/echo', payload) + + expect(response.headers['Content-Type']).to start_with('multipart/form-data') + end + end + + context 'when no multipart mime type is provided' do + let(:options) { {} } + + it_behaves_like 'a multipart request' + + it 'uses the default mime type' do + response = conn.post('/echo', payload) + + expect(response.headers['Content-Type']).to start_with('multipart/form-data') + end + end + end end