Skip to content

Commit

Permalink
Add The Ability To Specify A Different MIME Type
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
juna-nb committed Nov 1, 2024
1 parent b843898 commit ad034ef
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 10 deletions.
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
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
- 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::Multipart::FilePart` wraps binary file data with a Content-Type. The file data can be specified with a String path to a
- `Faraday::Multipart::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::Multipart::ParamPart` wraps a String value with a Content-Type, and optionally a Content-ID.
- `Faraday::Multipart::ParamPart` wraps a String value with a Content-Type, and optionally a Content-ID.

## Installation

Expand Down Expand Up @@ -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.

Expand Down
53 changes: 47 additions & 6 deletions lib/faraday/multipart/middleware.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# frozen_string_literal: true

require 'securerandom'
require "securerandom"

module Faraday
module Multipart
# Middleware for supporting multi-part requests.
class Middleware < Faraday::Request::UrlEncoded
DEFAULT_BOUNDARY_PREFIX = '-----------RubyMultipartPost'

self.mime_type = 'multipart/form-data'
class Middleware < Faraday::Middleware
CONTENT_TYPE = 'Content-Type'
DEFAULT_BOUNDARY_PREFIX = "-----------RubyMultipartPost"

def initialize(app = nil, options = {})
super(app)
Expand All @@ -28,15 +27,46 @@ 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]
#
# @return [Boolean] True if the request has a body and its Content-Type is
# urlencoded.
def process_request?(env)
type = request_type(env)
env.body && (type.empty? || (type == mime_type))
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]
Expand Down Expand Up @@ -97,6 +127,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.start_with?(/multipart\/.+/)
@options[:content_type].to_s
else
'multipart/form-data'
end
end
end
end
end
45 changes: 45 additions & 0 deletions spec/faraday/multipart/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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('<xml><value /></xml>', '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

0 comments on commit ad034ef

Please sign in to comment.