mirror of
https://github.com/lostisland/faraday.git
synced 2025-08-10 00:03:15 -04:00
extract multipart stuff from Adapter to Request::Multipart middleware
This commit is contained in:
parent
862c98344f
commit
d6d86dd043
@ -1,11 +1,7 @@
|
||||
module Faraday
|
||||
class Adapter < Middleware
|
||||
FORM_TYPE = 'application/x-www-form-urlencoded'.freeze
|
||||
MULTIPART_TYPE = 'multipart/form-data'.freeze
|
||||
CONTENT_TYPE = 'Content-Type'.freeze
|
||||
DEFAULT_BOUNDARY = "-----------RubyMultipartPost".freeze
|
||||
|
||||
extend AutoloadHelper
|
||||
|
||||
autoload_all 'faraday/adapter',
|
||||
:ActionDispatch => 'action_dispatch',
|
||||
:NetHttp => 'net_http',
|
||||
@ -27,75 +23,7 @@ module Faraday
|
||||
:logger => :Logger
|
||||
|
||||
def call(env)
|
||||
process_body_for_request(env)
|
||||
end
|
||||
|
||||
# Converts a body hash into encoded form params. This is done as late
|
||||
# as possible in the request cycle in case some other middleware wants to
|
||||
# act on the request before sending it out.
|
||||
#
|
||||
# env - The current request environment Hash.
|
||||
# body - A Hash of keys/values. Strings and empty values will be
|
||||
# ignored. Default: env[:body]
|
||||
# headers - The Hash of request headers. Default: env[:request_headers]
|
||||
#
|
||||
# Returns nothing. If the body is processed, it is replaced in the
|
||||
# environment for you.
|
||||
def process_body_for_request(env, body = env[:body], headers = env[:request_headers])
|
||||
return if body.nil? || body.empty? || !body.respond_to?(:each_key)
|
||||
if has_multipart?(body)
|
||||
env[:request] ||= {}
|
||||
env[:request][:boundary] ||= DEFAULT_BOUNDARY
|
||||
headers[CONTENT_TYPE] = MULTIPART_TYPE + ";boundary=#{env[:request][:boundary]}"
|
||||
env[:body] = create_multipart(env, body)
|
||||
else
|
||||
type = headers[CONTENT_TYPE]
|
||||
headers[CONTENT_TYPE] = FORM_TYPE if type.nil? || type.empty?
|
||||
parts = []
|
||||
process_to_params(parts, env[:body]) do |key, value|
|
||||
"#{key}=#{escape(value.to_s)}"
|
||||
end
|
||||
env[:body] = parts * "&"
|
||||
end
|
||||
end
|
||||
|
||||
def has_multipart?(body)
|
||||
body.values.each do |v|
|
||||
if v.respond_to?(:content_type)
|
||||
return true
|
||||
elsif v.respond_to?(:values)
|
||||
return true if has_multipart?(v)
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def create_multipart(env, params, boundary = nil)
|
||||
boundary ||= env[:request][:boundary]
|
||||
parts = []
|
||||
process_to_params(parts, params) do |key, value|
|
||||
Faraday::Parts::Part.new(boundary, key, value)
|
||||
end
|
||||
parts << Faraday::Parts::EpiloguePart.new(boundary)
|
||||
env[:request_headers]['Content-Length'] = parts.inject(0) {|sum,i| sum + i.length }.to_s
|
||||
Faraday::CompositeReadIO.new(*parts.map{|p| p.to_io })
|
||||
end
|
||||
|
||||
def process_to_params(pieces, params, base = nil)
|
||||
params.to_a.each do |key, value|
|
||||
key_str = base ? "#{base}[#{key}]" : key
|
||||
|
||||
block = block_given? ? Proc.new : nil
|
||||
case value
|
||||
when Array
|
||||
values = value.inject([]) { |a,v| a << [nil, v] }
|
||||
process_to_params(pieces, values, key_str, &block)
|
||||
when Hash
|
||||
process_to_params(pieces, value, key_str, &block)
|
||||
else
|
||||
pieces << block.call(key_str, value)
|
||||
end
|
||||
end
|
||||
# do nothing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -27,12 +27,6 @@ module Faraday
|
||||
:body => resp.body
|
||||
@app.call env
|
||||
end
|
||||
|
||||
# TODO: build in support for multipart streaming if action dispatch supports it.
|
||||
def create_multipart(env, params, boundary = nil)
|
||||
stream = super
|
||||
stream.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -23,7 +23,7 @@ module Faraday
|
||||
end
|
||||
|
||||
def call(env)
|
||||
process_body_for_request(env)
|
||||
super
|
||||
request = EventMachine::HttpRequest.new(URI::parse(env[:url].to_s))
|
||||
options = {:head => env[:request_headers]}
|
||||
options[:ssl] = env[:ssl] if env[:ssl]
|
||||
|
@ -10,6 +10,9 @@ module Faraday
|
||||
def call(env)
|
||||
super
|
||||
|
||||
# TODO: support streaming requests
|
||||
env[:body] = env[:body].read if env[:body].respond_to? :read
|
||||
|
||||
sess = ::Patron::Session.new
|
||||
args = [env[:method], env[:url].to_s, env[:request_headers]]
|
||||
if Faraday::Connection::METHODS_WITH_BODIES.include?(env[:method])
|
||||
@ -27,12 +30,6 @@ module Faraday
|
||||
rescue Errno::ECONNREFUSED
|
||||
raise Error::ConnectionFailed, $!
|
||||
end
|
||||
|
||||
# TODO: build in support for multipart streaming if patron supports it.
|
||||
def create_multipart(env, params, boundary = nil)
|
||||
stream = super
|
||||
stream.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -118,11 +118,6 @@ module Faraday
|
||||
end
|
||||
@app.call(env)
|
||||
end
|
||||
|
||||
def create_multipart(env, params, boundary = nil)
|
||||
stream = super
|
||||
stream.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -16,7 +16,10 @@ module Faraday
|
||||
def call(env)
|
||||
super
|
||||
|
||||
req = ::Typhoeus::Request.new env[:url].to_s,
|
||||
# TODO: support streaming requests
|
||||
env[:body] = env[:body].read if env[:body].respond_to? :read
|
||||
|
||||
req = ::Typhoeus::Request.new env[:url].to_s,
|
||||
:method => env[:method],
|
||||
:body => env[:body],
|
||||
:headers => env[:request_headers],
|
||||
@ -62,12 +65,6 @@ module Faraday
|
||||
reject { |(k, v)| k.nil? }. # Ignore blank lines
|
||||
map { |(k, v)| [k.downcase, v] }.flatten]
|
||||
end
|
||||
|
||||
# TODO: build in support for multipart streaming if typhoeus supports it.
|
||||
def create_multipart(env, params, boundary = nil)
|
||||
stream = super
|
||||
stream.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -77,8 +77,11 @@ module Faraday
|
||||
end
|
||||
|
||||
def to_app
|
||||
# use at least an adapter so the stack isn't a no-op
|
||||
self.adapter Faraday.default_adapter if @handlers.empty?
|
||||
# default stack, if nothing else is configured
|
||||
if @handlers.empty?
|
||||
self.request :url_encoded
|
||||
self.adapter Faraday.default_adapter
|
||||
end
|
||||
# last added handler should be the deepest, closest to the inner app
|
||||
@handlers.reverse.inject(@inner_app) { |app, handler| handler.build(app) }
|
||||
end
|
||||
|
@ -14,11 +14,13 @@ module Faraday
|
||||
|
||||
autoload_all 'faraday/request',
|
||||
:JSON => 'json',
|
||||
:UrlEncoded => 'url_encoded'
|
||||
:UrlEncoded => 'url_encoded',
|
||||
:Multipart => 'multipart'
|
||||
|
||||
register_lookup_modules \
|
||||
:json => :JSON,
|
||||
:url_encoded => :UrlEncoded
|
||||
:url_encoded => :UrlEncoded,
|
||||
:multipart => :Multipart
|
||||
|
||||
def self.run(connection, request_method)
|
||||
req = create
|
||||
|
63
lib/faraday/request/multipart.rb
Normal file
63
lib/faraday/request/multipart.rb
Normal file
@ -0,0 +1,63 @@
|
||||
module Faraday
|
||||
class Request::Multipart < Request::UrlEncoded
|
||||
self.mime_type = 'multipart/form-data'.freeze
|
||||
DEFAULT_BOUNDARY = "-----------RubyMultipartPost".freeze
|
||||
|
||||
def call(env)
|
||||
match_content_type(env) do |params|
|
||||
env[:request] ||= {}
|
||||
env[:request][:boundary] ||= DEFAULT_BOUNDARY
|
||||
env[:request_headers][CONTENT_TYPE] += ";boundary=#{env[:request][:boundary]}"
|
||||
env[:body] = create_multipart(env, params)
|
||||
end
|
||||
@app.call env
|
||||
end
|
||||
|
||||
def process_request?(env)
|
||||
type = request_type(env)
|
||||
env[:body].respond_to?(:each_key) and !env[:body].empty? and (
|
||||
(type.empty? and has_multipart?(env[:body])) or
|
||||
type == self.class.mime_type
|
||||
)
|
||||
end
|
||||
|
||||
def has_multipart?(body)
|
||||
body.values.each do |val|
|
||||
if val.respond_to?(:content_type)
|
||||
return true
|
||||
elsif val.respond_to?(:values)
|
||||
return true if has_multipart?(val)
|
||||
end
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def create_multipart(env, params)
|
||||
boundary = env[:request][:boundary]
|
||||
parts = process_params(params) do |key, value|
|
||||
Faraday::Parts::Part.new(boundary, key, value)
|
||||
end
|
||||
parts << Faraday::Parts::EpiloguePart.new(boundary)
|
||||
|
||||
body = Faraday::CompositeReadIO.new(parts)
|
||||
env[:request_headers]['Content-Length'] = body.length.to_s
|
||||
return body
|
||||
end
|
||||
|
||||
def process_params(params, prefix = nil, pieces = nil, &block)
|
||||
params.inject(pieces || []) do |all, (key, value)|
|
||||
key = "#{prefix}[#{key}]" if prefix
|
||||
|
||||
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
|
||||
all << block.call(key, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,9 +1,10 @@
|
||||
module Faraday
|
||||
class Request::UrlEncoded < Faraday::Middleware
|
||||
CONTENT_TYPE = 'Content-Type'.freeze
|
||||
|
||||
class << self
|
||||
attr_accessor :mime_type
|
||||
end
|
||||
|
||||
self.mime_type = 'application/x-www-form-urlencoded'.freeze
|
||||
|
||||
def call(env)
|
||||
@ -16,14 +17,19 @@ module Faraday
|
||||
def match_content_type(env)
|
||||
type = request_type(env)
|
||||
|
||||
if env[:body] and (type.empty? or type == self.class.mime_type)
|
||||
env[:request_headers]['Content-Type'] ||= self.class.mime_type
|
||||
if process_request?(env)
|
||||
env[:request_headers][CONTENT_TYPE] ||= self.class.mime_type
|
||||
yield env[:body] unless env[:body].respond_to?(:to_str)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def process_request?(env)
|
||||
type = request_type(env)
|
||||
env[:body] and (type.empty? or type == self.class.mime_type)
|
||||
end
|
||||
|
||||
def request_type(env)
|
||||
type = env[:request_headers]['Content-Type'].to_s
|
||||
type = env[:request_headers][CONTENT_TYPE].to_s
|
||||
type = type.split(';', 2).first if type.index(';')
|
||||
type
|
||||
end
|
||||
|
@ -3,13 +3,21 @@ begin
|
||||
require 'parts'
|
||||
require 'stringio'
|
||||
rescue LoadError
|
||||
puts "Install the multipart-post gem."
|
||||
$stderr.puts "Install the multipart-post gem."
|
||||
raise
|
||||
end
|
||||
|
||||
# Auto-load multipart-post gem on first request.
|
||||
module Faraday
|
||||
CompositeReadIO = ::CompositeReadIO
|
||||
UploadIO = ::UploadIO
|
||||
Parts = ::Parts
|
||||
end
|
||||
class CompositeReadIO < ::CompositeReadIO
|
||||
attr_reader :length
|
||||
|
||||
def initialize(parts)
|
||||
@length = parts.inject(0) { |sum, part| sum + part.length }
|
||||
ios = parts.map{ |part| part.to_io }
|
||||
super(*ios)
|
||||
end
|
||||
end
|
||||
|
||||
UploadIO = ::UploadIO
|
||||
Parts = ::Parts
|
||||
end
|
||||
|
@ -45,13 +45,12 @@ else
|
||||
end
|
||||
|
||||
define_method "test_#{adapter}_POST_sends_files" do
|
||||
name = File.join(File.dirname(__FILE__), '..', 'live_server.rb')
|
||||
resp = create_connection(adapter).post do |req|
|
||||
req.url 'file'
|
||||
req.body = {'uploaded_file' => Faraday::UploadIO.new(name, 'text/x-ruby')}
|
||||
req.body = {'uploaded_file' => Faraday::UploadIO.new(__FILE__, 'text/x-ruby')}
|
||||
end
|
||||
assert_equal "file live_server.rb text/x-ruby", resp.body
|
||||
end
|
||||
assert_equal "file live_test.rb text/x-ruby", resp.body
|
||||
end unless :default == adapter # isn't configured for multipart
|
||||
|
||||
# http://github.com/toland/patron/issues/#issue/9
|
||||
if ENV['FORCE'] || adapter != Faraday::Adapter::Patron
|
||||
@ -144,11 +143,15 @@ else
|
||||
|
||||
def create_connection(adapter)
|
||||
if adapter == :default
|
||||
conn = Faraday.default_connection
|
||||
conn.url_prefix = LIVE_SERVER
|
||||
conn
|
||||
Faraday.default_connection.tap do |conn|
|
||||
conn.builder.to_app # trigger default stack
|
||||
conn.url_prefix = LIVE_SERVER
|
||||
conn.headers['X-Faraday-Adapter'] = adapter.to_s
|
||||
end
|
||||
else
|
||||
Faraday::Connection.new LIVE_SERVER do |b|
|
||||
Faraday::Connection.new LIVE_SERVER, :headers => {'X-Faraday-Adapter' => adapter.to_s} do |b|
|
||||
b.request :multipart
|
||||
b.request :url_encoded
|
||||
b.use adapter
|
||||
end
|
||||
end
|
||||
|
@ -1,5 +1,4 @@
|
||||
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'helper'))
|
||||
require 'webmock/test_unit'
|
||||
|
||||
module Adapters
|
||||
class NetHttpTest < Faraday::TestCase
|
||||
|
@ -1,58 +0,0 @@
|
||||
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
||||
|
||||
class FormPostTest < Faraday::TestCase
|
||||
def setup
|
||||
@app = Faraday::Adapter.new nil
|
||||
@env = {:request_headers => {}}
|
||||
end
|
||||
|
||||
def test_processes_nested_body
|
||||
@env[:body] = {:a => 1, :b => {:c => 2}}
|
||||
@app.process_body_for_request @env
|
||||
assert_match /^|\&a=1/, @env[:body]
|
||||
assert_match /^|\&b\[c\]=2/, @env[:body]
|
||||
assert_equal Faraday::Adapter::FORM_TYPE, @env[:request_headers]['Content-Type']
|
||||
end
|
||||
|
||||
def test_processes_with_custom_type
|
||||
@env[:body] = {:a => 1}
|
||||
@env[:request_headers]['Content-Type'] = 'test/type'
|
||||
@app.process_body_for_request @env
|
||||
assert_equal 'a=1', @env[:body]
|
||||
assert_equal 'test/type', @env[:request_headers]['Content-Type']
|
||||
end
|
||||
|
||||
def test_processes_nil_body
|
||||
@env[:body] = nil
|
||||
@app.process_body_for_request @env
|
||||
assert_nil @env[:body]
|
||||
end
|
||||
|
||||
def test_processes_empty_body
|
||||
@env[:body] = ''
|
||||
@app.process_body_for_request @env
|
||||
assert_equal '', @env[:body]
|
||||
end
|
||||
|
||||
def test_processes_string_body
|
||||
@env[:body] = 'abc'
|
||||
@app.process_body_for_request @env
|
||||
assert_equal 'abc', @env[:body]
|
||||
end
|
||||
|
||||
def test_processes_array_values
|
||||
@env[:body] = {:a => [:b, 1]}
|
||||
@app.process_body_for_request @env
|
||||
assert_equal 'a[]=b&a[]=1', @env[:body]
|
||||
end
|
||||
|
||||
def test_processes_nested_array_values
|
||||
@env[:body] = {:a => [:b, {:c => :d}, [:e]]}
|
||||
@app.process_body_for_request @env
|
||||
|
||||
# a[]=b&a[][c]=d&a[][]=e
|
||||
assert_match /a\[\]=b/, @env[:body]
|
||||
assert_match /a\[\]\[c\]=d/, @env[:body]
|
||||
assert_match /a\[\]\[\]=e/, @env[:body]
|
||||
end
|
||||
end
|
@ -1,5 +1,8 @@
|
||||
require 'rubygems'
|
||||
require 'test/unit'
|
||||
require 'webmock/test_unit'
|
||||
|
||||
WebMock.disable_net_connect!(:allow_localhost => true)
|
||||
|
||||
if ENV['LEFTRIGHT']
|
||||
begin
|
||||
|
@ -9,9 +9,13 @@ get '/json' do
|
||||
end
|
||||
|
||||
post '/file' do
|
||||
"file %s %s" % [
|
||||
params[:uploaded_file][:filename],
|
||||
params[:uploaded_file][:type]]
|
||||
if params[:uploaded_file].respond_to? :each_key
|
||||
"file %s %s" % [
|
||||
params[:uploaded_file][:filename],
|
||||
params[:uploaded_file][:type]]
|
||||
else
|
||||
status 400
|
||||
end
|
||||
end
|
||||
|
||||
post '/hello' do
|
||||
|
@ -19,8 +19,10 @@ class MiddlewareStackTest < Faraday::TestCase
|
||||
|
||||
def test_sets_default_adapter_if_none_set
|
||||
@conn.to_app
|
||||
default_middleware = Faraday::Request.lookup_module :url_encoded
|
||||
default_adapter_klass = Faraday::Adapter.lookup_module Faraday.default_adapter
|
||||
assert @builder[0] == default_adapter_klass
|
||||
assert @builder[0] == default_middleware
|
||||
assert @builder[1] == default_adapter_klass
|
||||
end
|
||||
|
||||
def test_allows_rebuilding
|
||||
|
@ -1,48 +0,0 @@
|
||||
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
||||
|
||||
Faraday::CompositeReadIO.send :attr_reader, :ios
|
||||
|
||||
class MultipartTest < Faraday::TestCase
|
||||
def setup
|
||||
@app = Faraday::Adapter.new nil
|
||||
@env = {:request_headers => {}}
|
||||
end
|
||||
|
||||
def test_processes_nested_body
|
||||
# assume params are out of order
|
||||
regexes = [
|
||||
/name\=\"a\"/,
|
||||
/name=\"b\[c\]\"\; filename\=\"multipart_test\.rb\"/,
|
||||
/name=\"b\[d\]\"/]
|
||||
@env[:body] = {:a => 1, :b => {:c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2}}
|
||||
@app.process_body_for_request @env
|
||||
@env[:body].send(:ios).map(&:read).each do |io|
|
||||
if re = regexes.detect { |r| io =~ r }
|
||||
regexes.delete re
|
||||
end
|
||||
end
|
||||
assert_equal [], regexes
|
||||
assert_kind_of CompositeReadIO, @env[:body]
|
||||
assert_equal "%s;boundary=%s" %
|
||||
[Faraday::Adapter::MULTIPART_TYPE, Faraday::Adapter::DEFAULT_BOUNDARY],
|
||||
@env[:request_headers]['Content-Type']
|
||||
end
|
||||
|
||||
def test_processes_nil_body
|
||||
@env[:body] = nil
|
||||
@app.process_body_for_request @env
|
||||
assert_nil @env[:body]
|
||||
end
|
||||
|
||||
def test_processes_empty_body
|
||||
@env[:body] = ''
|
||||
@app.process_body_for_request @env
|
||||
assert_equal '', @env[:body]
|
||||
end
|
||||
|
||||
def test_processes_string_body
|
||||
@env[:body] = 'abc'
|
||||
@app.process_body_for_request @env
|
||||
assert_equal 'abc', @env[:body]
|
||||
end
|
||||
end
|
@ -1,9 +1,12 @@
|
||||
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
||||
require 'rack/utils'
|
||||
|
||||
Faraday::CompositeReadIO.send :attr_reader, :ios
|
||||
|
||||
class RequestMiddlewareTest < Faraday::TestCase
|
||||
def setup
|
||||
@conn = Faraday.new do |b|
|
||||
b.request :multipart
|
||||
b.request :url_encoded
|
||||
b.request :json
|
||||
b.adapter :test do |stub|
|
||||
@ -57,4 +60,26 @@ class RequestMiddlewareTest < Faraday::TestCase
|
||||
expected = { 'user' => {'name' => 'Mislav', 'web' => 'mislav.net'} }
|
||||
assert_equal expected, Rack::Utils.parse_nested_query(response.body)
|
||||
end
|
||||
|
||||
def test_multipart
|
||||
# assume params are out of order
|
||||
regexes = [
|
||||
/name\=\"a\"/,
|
||||
/name=\"b\[c\]\"\; filename\=\"request_middleware_test\.rb\"/,
|
||||
/name=\"b\[d\]\"/]
|
||||
|
||||
payload = {:a => 1, :b => {:c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2}}
|
||||
response = @conn.post('/echo', payload)
|
||||
|
||||
assert_kind_of Faraday::CompositeReadIO, response.body
|
||||
assert_equal "multipart/form-data;boundary=%s" % Faraday::Request::Multipart::DEFAULT_BOUNDARY,
|
||||
response.headers['Content-Type']
|
||||
|
||||
response.body.send(:ios).map(&:read).each do |io|
|
||||
if re = regexes.detect { |r| io =~ r }
|
||||
regexes.delete re
|
||||
end
|
||||
end
|
||||
assert_equal [], regexes
|
||||
end
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user