faraday 0.9.2 → 0.17.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +232 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +168 -18
  5. data/Rakefile +13 -0
  6. data/lib/faraday/adapter/em_http.rb +17 -11
  7. data/lib/faraday/adapter/em_http_ssl_patch.rb +1 -1
  8. data/lib/faraday/adapter/em_synchrony.rb +13 -7
  9. data/lib/faraday/adapter/excon.rb +12 -11
  10. data/lib/faraday/adapter/httpclient.rb +21 -10
  11. data/lib/faraday/adapter/net_http.rb +36 -13
  12. data/lib/faraday/adapter/net_http_persistent.rb +36 -17
  13. data/lib/faraday/adapter/patron.rb +36 -19
  14. data/lib/faraday/adapter/rack.rb +1 -1
  15. data/lib/faraday/adapter/test.rb +79 -28
  16. data/lib/faraday/adapter/typhoeus.rb +4 -115
  17. data/lib/faraday/adapter.rb +10 -1
  18. data/lib/faraday/autoload.rb +1 -1
  19. data/lib/faraday/connection.rb +64 -17
  20. data/lib/faraday/deprecate.rb +109 -0
  21. data/lib/faraday/error.rb +132 -27
  22. data/lib/faraday/options.rb +36 -22
  23. data/lib/faraday/parameters.rb +2 -1
  24. data/lib/faraday/rack_builder.rb +26 -2
  25. data/lib/faraday/request/authorization.rb +1 -2
  26. data/lib/faraday/request/multipart.rb +7 -2
  27. data/lib/faraday/request/retry.rb +77 -18
  28. data/lib/faraday/request.rb +24 -0
  29. data/lib/faraday/response/logger.rb +29 -8
  30. data/lib/faraday/response/raise_error.rb +7 -3
  31. data/lib/faraday/response.rb +7 -3
  32. data/lib/faraday/upload_io.rb +16 -6
  33. data/lib/faraday/utils.rb +19 -2
  34. data/lib/faraday.rb +15 -36
  35. data/spec/faraday/deprecate_spec.rb +147 -0
  36. data/spec/faraday/error_spec.rb +102 -0
  37. data/spec/faraday/response/raise_error_spec.rb +106 -0
  38. data/spec/spec_helper.rb +105 -0
  39. data/test/adapters/default_test.rb +14 -0
  40. data/test/adapters/em_http_test.rb +30 -0
  41. data/test/adapters/em_synchrony_test.rb +32 -0
  42. data/test/adapters/excon_test.rb +30 -0
  43. data/test/adapters/httpclient_test.rb +34 -0
  44. data/test/adapters/integration.rb +263 -0
  45. data/test/adapters/logger_test.rb +136 -0
  46. data/test/adapters/net_http_persistent_test.rb +114 -0
  47. data/test/adapters/net_http_test.rb +79 -0
  48. data/test/adapters/patron_test.rb +40 -0
  49. data/test/adapters/rack_test.rb +38 -0
  50. data/test/adapters/test_middleware_test.rb +157 -0
  51. data/test/adapters/typhoeus_test.rb +38 -0
  52. data/test/authentication_middleware_test.rb +65 -0
  53. data/test/composite_read_io_test.rb +109 -0
  54. data/test/connection_test.rb +738 -0
  55. data/test/env_test.rb +268 -0
  56. data/test/helper.rb +75 -0
  57. data/test/live_server.rb +67 -0
  58. data/test/middleware/instrumentation_test.rb +88 -0
  59. data/test/middleware/retry_test.rb +282 -0
  60. data/test/middleware_stack_test.rb +260 -0
  61. data/test/multibyte.txt +1 -0
  62. data/test/options_test.rb +333 -0
  63. data/test/parameters_test.rb +157 -0
  64. data/test/request_middleware_test.rb +126 -0
  65. data/test/response_middleware_test.rb +72 -0
  66. data/test/strawberry.rb +2 -0
  67. data/test/utils_test.rb +98 -0
  68. metadata +81 -63
@@ -3,11 +3,6 @@ module Faraday
3
3
  class Excon < Faraday::Adapter
4
4
  dependency 'excon'
5
5
 
6
- def initialize(app, connection_options = {})
7
- @connection_options = connection_options
8
- super(app)
9
- end
10
-
11
6
  def call(env)
12
7
  super
13
8
 
@@ -20,6 +15,9 @@ module Faraday
20
15
  opts[:client_key] = ssl[:client_key] if ssl[:client_key]
21
16
  opts[:certificate] = ssl[:certificate] if ssl[:certificate]
22
17
  opts[:private_key] = ssl[:private_key] if ssl[:private_key]
18
+ opts[:ssl_version] = ssl[:version] if ssl[:version]
19
+ opts[:ssl_min_version] = ssl[:min_version] if ssl[:min_version]
20
+ opts[:ssl_max_version] = ssl[:max_version] if ssl[:max_version]
23
21
 
24
22
  # https://github.com/geemus/excon/issues/106
25
23
  # https://github.com/jruby/jruby-ossl/issues/19
@@ -35,7 +33,6 @@ module Faraday
35
33
 
36
34
  if req[:open_timeout]
37
35
  opts[:connect_timeout] = req[:open_timeout]
38
- opts[:write_timeout] = req[:open_timeout]
39
36
  end
40
37
 
41
38
  if req[:proxy]
@@ -50,26 +47,30 @@ module Faraday
50
47
  end
51
48
  end
52
49
 
53
- conn = ::Excon.new(env[:url].to_s, opts.merge(@connection_options))
50
+ conn = create_connection(env, opts)
54
51
 
55
52
  resp = conn.request \
56
53
  :method => env[:method].to_s.upcase,
57
54
  :headers => env[:request_headers],
58
55
  :body => read_body(env)
59
56
 
60
- save_response(env, resp.status.to_i, resp.body, resp.headers)
57
+ save_response(env, resp.status.to_i, resp.body, resp.headers, resp.reason_phrase)
61
58
 
62
59
  @app.call env
63
60
  rescue ::Excon::Errors::SocketError => err
64
61
  if err.message =~ /\btimeout\b/
65
- raise Error::TimeoutError, err
62
+ raise Faraday::TimeoutError, err
66
63
  elsif err.message =~ /\bcertificate\b/
67
64
  raise Faraday::SSLError, err
68
65
  else
69
- raise Error::ConnectionFailed, err
66
+ raise Faraday::ConnectionFailed, err
70
67
  end
71
68
  rescue ::Excon::Errors::Timeout => err
72
- raise Error::TimeoutError, err
69
+ raise Faraday::TimeoutError, err
70
+ end
71
+
72
+ def create_connection(env, opts)
73
+ ::Excon.new(env[:url].to_s, opts.merge(@connection_options))
73
74
  end
74
75
 
75
76
  # TODO: support streaming requests
@@ -29,6 +29,8 @@ module Faraday
29
29
  configure_ssl ssl
30
30
  end
31
31
 
32
+ configure_client
33
+
32
34
  # TODO Don't stream yet.
33
35
  # https://github.com/nahi/httpclient/pull/90
34
36
  env[:body] = env[:body].read if env[:body].respond_to? :read
@@ -37,19 +39,19 @@ module Faraday
37
39
  :body => env[:body],
38
40
  :header => env[:request_headers]
39
41
 
40
- save_response env, resp.status, resp.body, resp.headers
42
+ save_response env, resp.status, resp.body, resp.headers, resp.reason
41
43
 
42
44
  @app.call env
43
45
  rescue ::HTTPClient::TimeoutError, Errno::ETIMEDOUT
44
- raise Faraday::Error::TimeoutError, $!
46
+ raise Faraday::TimeoutError, $!
45
47
  rescue ::HTTPClient::BadResponseError => err
46
48
  if err.message.include?('status 407')
47
- raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
49
+ raise Faraday::ConnectionFailed, %{407 "Proxy Authentication Required "}
48
50
  else
49
- raise Faraday::Error::ClientError, $!
51
+ raise Faraday::ClientError, $!
50
52
  end
51
- rescue Errno::ECONNREFUSED, EOFError
52
- raise Faraday::Error::ConnectionFailed, $!
53
+ rescue Errno::ECONNREFUSED, IOError, SocketError
54
+ raise Faraday::ConnectionFailed, $!
53
55
  rescue => err
54
56
  if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
55
57
  raise Faraday::SSLError, err
@@ -95,12 +97,21 @@ module Faraday
95
97
  end
96
98
  end
97
99
 
100
+ def configure_client
101
+ @config_block.call(client) if @config_block
102
+ end
103
+
98
104
  def ssl_cert_store(ssl)
99
105
  return ssl[:cert_store] if ssl[:cert_store]
100
- # Use the default cert store by default, i.e. system ca certs
101
- cert_store = OpenSSL::X509::Store.new
102
- cert_store.set_default_paths
103
- cert_store
106
+ # Memoize the cert store so that the same one is passed to
107
+ # HTTPClient each time, to avoid resyncing SSL sesions when
108
+ # it's changed
109
+ @cert_store ||= begin
110
+ # Use the default cert store by default, i.e. system ca certs
111
+ cert_store = OpenSSL::X509::Store.new
112
+ cert_store.set_default_paths
113
+ cert_store
114
+ end
104
115
  end
105
116
 
106
117
  def ssl_verify_mode(ssl)
@@ -10,13 +10,15 @@ module Faraday
10
10
  class Adapter
11
11
  class NetHttp < Faraday::Adapter
12
12
  NET_HTTP_EXCEPTIONS = [
13
- EOFError,
13
+ IOError,
14
+ Errno::EADDRNOTAVAIL,
14
15
  Errno::ECONNABORTED,
15
16
  Errno::ECONNREFUSED,
16
17
  Errno::ECONNRESET,
17
18
  Errno::EHOSTUNREACH,
18
19
  Errno::EINVAL,
19
20
  Errno::ENETUNREACH,
21
+ Errno::EPIPE,
20
22
  Net::HTTPBadResponse,
21
23
  Net::HTTPHeaderSyntaxError,
22
24
  Net::ProtocolError,
@@ -27,14 +29,16 @@ module Faraday
27
29
  NET_HTTP_EXCEPTIONS << OpenSSL::SSL::SSLError if defined?(OpenSSL)
28
30
  NET_HTTP_EXCEPTIONS << Net::OpenTimeout if defined?(Net::OpenTimeout)
29
31
 
32
+ def initialize(app = nil, opts = {}, &block)
33
+ @cert_store = nil
34
+ super(app, opts, &block)
35
+ end
36
+
30
37
  def call(env)
31
38
  super
32
39
  with_net_http_connection(env) do |http|
33
40
  configure_ssl(http, env[:ssl]) if env[:url].scheme == 'https' and env[:ssl]
34
-
35
- req = env[:request]
36
- http.read_timeout = http.open_timeout = req[:timeout] if req[:timeout]
37
- http.open_timeout = req[:open_timeout] if req[:open_timeout]
41
+ configure_request(http, env[:request])
38
42
 
39
43
  begin
40
44
  http_response = perform_request(http, env)
@@ -42,11 +46,11 @@ module Faraday
42
46
  if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
43
47
  raise Faraday::SSLError, err
44
48
  else
45
- raise Error::ConnectionFailed, err
49
+ raise Faraday::ConnectionFailed, err
46
50
  end
47
51
  end
48
52
 
49
- save_response(env, http_response.code.to_i, http_response.body || '') do |response_headers|
53
+ save_response(env, http_response.code.to_i, http_response.body || '', nil, http_response.message) do |response_headers|
50
54
  http_response.each_header do |key, value|
51
55
  response_headers[key] = value
52
56
  end
@@ -55,9 +59,11 @@ module Faraday
55
59
 
56
60
  @app.call env
57
61
  rescue Timeout::Error, Errno::ETIMEDOUT => err
58
- raise Faraday::Error::TimeoutError, err
62
+ raise Faraday::TimeoutError, err
59
63
  end
60
64
 
65
+ private
66
+
61
67
  def create_request(env)
62
68
  request = Net::HTTPGenericRequest.new \
63
69
  env[:method].to_s.upcase, # request method
@@ -89,10 +95,10 @@ module Faraday
89
95
 
90
96
  def net_http_connection(env)
91
97
  if proxy = env[:request][:proxy]
92
- Net::HTTP::Proxy(proxy[:uri].host, proxy[:uri].port, proxy[:user], proxy[:password])
98
+ Net::HTTP::Proxy(proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
93
99
  else
94
100
  Net::HTTP
95
- end.new(env[:url].host, env[:url].port || (env[:url].scheme == 'https' ? 443 : 80))
101
+ end.new(env[:url].hostname, env[:url].port || (env[:url].scheme == 'https' ? 443 : 80))
96
102
  end
97
103
 
98
104
  def configure_ssl(http, ssl)
@@ -106,14 +112,31 @@ module Faraday
106
112
  http.ca_path = ssl[:ca_path] if ssl[:ca_path]
107
113
  http.verify_depth = ssl[:verify_depth] if ssl[:verify_depth]
108
114
  http.ssl_version = ssl[:version] if ssl[:version]
115
+ http.min_version = ssl[:min_version] if ssl[:min_version]
116
+ http.max_version = ssl[:max_version] if ssl[:max_version]
117
+ end
118
+
119
+ def configure_request(http, req)
120
+ if req[:timeout]
121
+ http.read_timeout = req[:timeout]
122
+ http.open_timeout = req[:timeout]
123
+ http.write_timeout = req[:timeout] if http.respond_to?(:write_timeout=)
124
+ end
125
+ http.open_timeout = req[:open_timeout] if req[:open_timeout]
126
+ http.write_timeout = req[:write_timeout] if req[:write_timeout] && http.respond_to?(:write_timeout=)
127
+ # Only set if Net::Http supports it, since Ruby 2.5.
128
+ http.max_retries = 0 if http.respond_to?(:max_retries=)
129
+
130
+ @config_block.call(http) if @config_block
109
131
  end
110
132
 
111
133
  def ssl_cert_store(ssl)
112
134
  return ssl[:cert_store] if ssl[:cert_store]
135
+ return @cert_store if @cert_store
113
136
  # Use the default cert store by default, i.e. system ca certs
114
- cert_store = OpenSSL::X509::Store.new
115
- cert_store.set_default_paths
116
- cert_store
137
+ @cert_store = OpenSSL::X509::Store.new
138
+ @cert_store.set_default_paths
139
+ @cert_store
117
140
  end
118
141
 
119
142
  def ssl_verify_mode(ssl)
@@ -1,14 +1,28 @@
1
- # Rely on autoloading instead of explicit require; helps avoid the "already
2
- # initialized constant" warning on Ruby 1.8.7 when NetHttp is refereced below.
3
- # require 'faraday/adapter/net_http'
4
-
5
1
  module Faraday
6
2
  class Adapter
7
3
  class NetHttpPersistent < NetHttp
8
4
  dependency 'net/http/persistent'
9
5
 
10
- def with_net_http_connection(env)
11
- if proxy = env[:request][:proxy]
6
+ private
7
+
8
+ def net_http_connection(env)
9
+ @cached_connection ||=
10
+ if Net::HTTP::Persistent.instance_method(:initialize).parameters.first == [:key, :name]
11
+ options = {name: 'Faraday'}
12
+ options[:pool_size] = @connection_options[:pool_size] if @connection_options.key?(:pool_size)
13
+ Net::HTTP::Persistent.new(**options)
14
+ else
15
+ Net::HTTP::Persistent.new('Faraday')
16
+ end
17
+
18
+ proxy_uri = proxy_uri(env)
19
+ @cached_connection.proxy = proxy_uri if @cached_connection.proxy_uri != proxy_uri
20
+ @cached_connection
21
+ end
22
+
23
+ def proxy_uri(env)
24
+ proxy_uri = nil
25
+ if (proxy = env[:request][:proxy])
12
26
  proxy_uri = ::URI::HTTP === proxy[:uri] ? proxy[:uri].dup : ::URI.parse(proxy[:uri].to_s)
13
27
  proxy_uri.user = proxy_uri.password = nil
14
28
  # awful patch for net-http-persistent 2.8 not unescaping user/password
@@ -17,32 +31,37 @@ module Faraday
17
31
  define_method(:password) { proxy[:password] }
18
32
  end if proxy[:user]
19
33
  end
20
-
21
- yield Net::HTTP::Persistent.new 'Faraday', proxy_uri
34
+ proxy_uri
22
35
  end
23
36
 
24
37
  def perform_request(http, env)
25
38
  http.request env[:url], create_request(env)
26
39
  rescue Errno::ETIMEDOUT => error
27
- raise Faraday::Error::TimeoutError, error
40
+ raise Faraday::TimeoutError, error
28
41
  rescue Net::HTTP::Persistent::Error => error
29
42
  if error.message.include? 'Timeout'
30
- raise Faraday::Error::TimeoutError, error
43
+ raise Faraday::TimeoutError, error
31
44
  elsif error.message.include? 'connection refused'
32
- raise Faraday::Error::ConnectionFailed, error
45
+ raise Faraday::ConnectionFailed, error
33
46
  else
34
47
  raise
35
48
  end
36
49
  end
37
50
 
38
51
  def configure_ssl(http, ssl)
39
- http.verify_mode = ssl_verify_mode(ssl)
40
- http.cert_store = ssl_cert_store(ssl)
52
+ http_set(http, :verify_mode, ssl_verify_mode(ssl))
53
+ http_set(http, :cert_store, ssl_cert_store(ssl))
54
+
55
+ http_set(http, :certificate, ssl[:client_cert]) if ssl[:client_cert]
56
+ http_set(http, :private_key, ssl[:client_key]) if ssl[:client_key]
57
+ http_set(http, :ca_file, ssl[:ca_file]) if ssl[:ca_file]
58
+ http_set(http, :ssl_version, ssl[:version]) if ssl[:version]
59
+ end
41
60
 
42
- http.certificate = ssl[:client_cert] if ssl[:client_cert]
43
- http.private_key = ssl[:client_key] if ssl[:client_key]
44
- http.ca_file = ssl[:ca_file] if ssl[:ca_file]
45
- http.ssl_version = ssl[:version] if ssl[:version]
61
+ def http_set(http, attr, value)
62
+ if http.send(attr) != value
63
+ http.send("#{attr}=", value)
64
+ end
46
65
  end
47
66
  end
48
67
  end
@@ -3,18 +3,14 @@ module Faraday
3
3
  class Patron < Faraday::Adapter
4
4
  dependency 'patron'
5
5
 
6
- def initialize(app, &block)
7
- super(app)
8
- @block = block
9
- end
10
-
11
6
  def call(env)
12
7
  super
13
-
14
8
  # TODO: support streaming requests
15
9
  env[:body] = env[:body].read if env[:body].respond_to? :read
16
10
 
17
- session = @session ||= create_session
11
+ session = ::Patron::Session.new
12
+ @config_block.call(session) if @config_block
13
+ configure_ssl(session, env[:ssl]) if env[:url].scheme == 'https' and env[:ssl]
18
14
 
19
15
  if req = env[:request]
20
16
  session.timeout = session.connect_timeout = req[:timeout] if req[:timeout]
@@ -32,23 +28,26 @@ module Faraday
32
28
  data = env[:body] ? env[:body].to_s : nil
33
29
  session.request(env[:method], env[:url].to_s, env[:request_headers], :data => data)
34
30
  rescue Errno::ECONNREFUSED, ::Patron::ConnectionFailed
35
- raise Error::ConnectionFailed, $!
31
+ raise Faraday::ConnectionFailed, $!
36
32
  end
37
33
 
38
- save_response(env, response.status, response.body, response.headers)
34
+ # Remove the "HTTP/1.1 200", leaving just the reason phrase
35
+ reason_phrase = response.status_line.gsub(/^.* \d{3} /, '')
36
+
37
+ save_response(env, response.status, response.body, response.headers, reason_phrase)
39
38
 
40
39
  @app.call env
41
40
  rescue ::Patron::TimeoutError => err
42
- if err.message == "Connection time-out"
43
- raise Faraday::Error::ConnectionFailed, err
41
+ if connection_timed_out_message?(err.message)
42
+ raise Faraday::ConnectionFailed, err
44
43
  else
45
- raise Faraday::Error::TimeoutError, err
44
+ raise Faraday::TimeoutError, err
46
45
  end
47
46
  rescue ::Patron::Error => err
48
47
  if err.message.include?("code 407")
49
- raise Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
48
+ raise Faraday::ConnectionFailed, %{407 "Proxy Authentication Required "}
50
49
  else
51
- raise Error::ConnectionFailed, err
50
+ raise Faraday::ConnectionFailed, err
52
51
  end
53
52
  end
54
53
 
@@ -67,12 +66,30 @@ module Faraday
67
66
  end
68
67
  end
69
68
 
70
- def create_session
71
- session = ::Patron::Session.new
72
- session.insecure = true
73
- @block.call(session) if @block
74
- session
69
+ def configure_ssl(session, ssl)
70
+ if ssl.fetch(:verify, true)
71
+ session.cacert = ssl[:ca_file]
72
+ else
73
+ session.insecure = true
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ CURL_TIMEOUT_MESSAGES = [ "Connection time-out",
80
+ "Connection timed out",
81
+ "Timed out before name resolve",
82
+ "server connect has timed out",
83
+ "Resolving timed out",
84
+ "name lookup timed out",
85
+ "timed out before SSL",
86
+ "connect() timed out"
87
+ ].freeze
88
+
89
+ def connection_timed_out_message?(message)
90
+ CURL_TIMEOUT_MESSAGES.any? { |curl_message| message.include?(curl_message) }
75
91
  end
92
+
76
93
  end
77
94
  end
78
95
  end
@@ -41,7 +41,7 @@ module Faraday
41
41
 
42
42
  timeout = env[:request][:timeout] || env[:request][:open_timeout]
43
43
  response = if timeout
44
- Timer.timeout(timeout, Faraday::Error::TimeoutError) { execute_request(env, rack_env) }
44
+ Timer.timeout(timeout, Faraday::TimeoutError) { execute_request(env, rack_env) }
45
45
  else
46
46
  execute_request(env, rack_env)
47
47
  end
@@ -1,16 +1,41 @@
1
1
  module Faraday
2
2
  class Adapter
3
- # test = Faraday::Connection.new do
4
- # use Faraday::Adapter::Test do |stub|
5
- # stub.get '/nigiri/sake.json' do
6
- # [200, {}, 'hi world']
3
+ # Examples
4
+ #
5
+ # test = Faraday::Connection.new do
6
+ # use Faraday::Adapter::Test do |stub|
7
+ # # simply define matcher to match the request
8
+ # stub.get '/resource.json' do
9
+ # # return static content
10
+ # [200, {'Content-Type' => 'application/json'}, 'hi world']
11
+ # end
12
+ #
13
+ # # response with content generated based on request
14
+ # stub.get '/showget' do |env|
15
+ # [200, {'Content-Type' => 'text/plain'}, env[:method].to_s]
16
+ # end
17
+ #
18
+ # # regular expression can be used as matching filter
19
+ # stub.get /\A\/items\/(\d+)\z/ do |env, meta|
20
+ # # in case regular expression is used an instance of MatchData can be received
21
+ # [200, {'Content-Type' => 'text/plain'}, "showing item: #{meta[:match_data][1]}"]
22
+ # end
7
23
  # end
8
24
  # end
9
- # end
10
- #
11
- # resp = test.get '/nigiri/sake.json'
12
- # resp.body # => 'hi world'
25
+ #
26
+ # resp = test.get '/resource.json'
27
+ # resp.body # => 'hi world'
28
+ #
29
+ # resp = test.get '/showget'
30
+ # resp.body # => 'get'
31
+ #
32
+ # resp = test.get '/items/1'
33
+ # resp.body # => 'showing item: 1'
34
+ #
35
+ # resp = test.get '/items/2'
36
+ # resp.body # => 'showing item: 2'
13
37
  #
38
+
14
39
  class Test < Faraday::Adapter
15
40
  attr_accessor :stubs
16
41
 
@@ -28,17 +53,17 @@ module Faraday
28
53
  @stack.empty?
29
54
  end
30
55
 
31
- def match(request_method, path, headers, body)
56
+ def match(request_method, host, path, headers, body)
32
57
  return false if !@stack.key?(request_method)
33
58
  stack = @stack[request_method]
34
59
  consumed = (@consumed[request_method] ||= [])
35
60
 
36
- if stub = matches?(stack, path, headers, body)
61
+ stub, meta = matches?(stack, host, path, headers, body)
62
+ if stub
37
63
  consumed << stack.delete(stub)
38
- stub
39
- else
40
- matches?(consumed, path, headers, body)
64
+ return stub, meta
41
65
  end
66
+ matches?(consumed, host, path, headers, body)
42
67
  end
43
68
 
44
69
  def get(path, headers = {}, &block)
@@ -85,33 +110,55 @@ module Faraday
85
110
  protected
86
111
 
87
112
  def new_stub(request_method, path, headers = {}, body=nil, &block)
88
- normalized_path = Faraday::Utils.normalize_path(path)
89
- (@stack[request_method] ||= []) << Stub.new(normalized_path, headers, body, block)
113
+ normalized_path, host =
114
+ if path.is_a?(Regexp)
115
+ path
116
+ else
117
+ [Faraday::Utils.normalize_path(path), Faraday::Utils.URI(path).host]
118
+ end
119
+
120
+ (@stack[request_method] ||= []) << Stub.new(host, normalized_path, headers, body, block)
90
121
  end
91
122
 
92
- def matches?(stack, path, headers, body)
93
- stack.detect { |stub| stub.matches?(path, headers, body) }
123
+ def matches?(stack, host, path, headers, body)
124
+ stack.each do |stub|
125
+ match_result, meta = stub.matches?(host, path, headers, body)
126
+ return stub, meta if match_result
127
+ end
128
+ nil
94
129
  end
95
130
  end
96
131
 
97
- class Stub < Struct.new(:path, :params, :headers, :body, :block)
98
- def initialize(full, headers, body, block)
99
- path, query = full.split('?')
132
+ class Stub < Struct.new(:host, :path, :params, :headers, :body, :block)
133
+ def initialize(host, full, headers, body, block)
134
+ path, query = full.respond_to?(:split) ? full.split("?") : full
100
135
  params = query ?
101
136
  Faraday::Utils.parse_nested_query(query) :
102
137
  {}
103
- super(path, params, headers, body, block)
138
+ super(host, path, params, headers, body, block)
104
139
  end
105
140
 
106
- def matches?(request_uri, request_headers, request_body)
141
+ def matches?(request_host, request_uri, request_headers, request_body)
107
142
  request_path, request_query = request_uri.split('?')
108
143
  request_params = request_query ?
109
144
  Faraday::Utils.parse_nested_query(request_query) :
110
145
  {}
111
- request_path == path &&
146
+ # meta is a hash use as carrier
147
+ # that will be yielded to consumer block
148
+ meta = {}
149
+ return (host.nil? || host == request_host) &&
150
+ path_match?(request_path, meta) &&
112
151
  params_match?(request_params) &&
113
152
  (body.to_s.size.zero? || request_body == body) &&
114
- headers_match?(request_headers)
153
+ headers_match?(request_headers), meta
154
+ end
155
+
156
+ def path_match?(request_path, meta)
157
+ if path.is_a? Regexp
158
+ !!(meta[:match_data] = path.match(request_path))
159
+ else
160
+ path == request_path
161
+ end
115
162
  end
116
163
 
117
164
  def params_match?(request_params)
@@ -143,14 +190,18 @@ module Faraday
143
190
 
144
191
  def call(env)
145
192
  super
193
+ host = env[:url].host
146
194
  normalized_path = Faraday::Utils.normalize_path(env[:url])
147
195
  params_encoder = env.request.params_encoder || Faraday::Utils.default_params_encoder
148
196
 
149
- if stub = stubs.match(env[:method], normalized_path, env.request_headers, env[:body])
197
+ stub, meta = stubs.match(env[:method], host, normalized_path, env.request_headers, env[:body])
198
+ if stub
150
199
  env[:params] = (query = env[:url].query) ?
151
- params_encoder.decode(query) :
152
- {}
153
- status, headers, body = stub.block.call(env)
200
+ params_encoder.decode(query) : {}
201
+ block_arity = stub.block.arity
202
+ status, headers, body = (block_arity >= 0) ?
203
+ stub.block.call(*[env, meta].take(block_arity)) :
204
+ stub.block.call(env, meta)
154
205
  save_response(env, status, body, headers)
155
206
  else
156
207
  raise Stubs::NotFound, "no stubbed request for #{env[:method]} #{normalized_path} #{env[:body]}"