cache middleware stack instead of rebuilding it on every request

The downside is, middleware can't be modified after making the first request:

  conn.use MyMiddleware       # => OK
  conn.get('/')
  conn.use AnotherMiddleware  # => raises a Builder::StackLocked error

On the plus side, the middleware stack is built only once and then cached as
the Connection#app object.

The Connection instance can always be "forked off" with the `dup` method and
modified as if it were fresh.
This commit is contained in:
Mislav Marohnić 2011-05-08 14:07:27 -07:00
parent 9ae912b2c8
commit b47fb2922f
5 changed files with 71 additions and 16 deletions

View File

@ -12,6 +12,9 @@ module Faraday
new { |builder| yield builder }
end
# Error raised when trying to modify the stack after calling `lock!`
class StackLocked < RuntimeError; end
# borrowed from ActiveSupport::Dependencies::Reference &
# ActionDispatch::MiddlewareStack::Middleware
class Handler
@ -55,6 +58,7 @@ module Faraday
end
def build(options = {})
raise_if_locked
@handlers.clear unless options[:keep]
yield self if block_given?
end
@ -76,7 +80,17 @@ module Faraday
@handlers.reverse.inject(inner_app) { |app, handler| handler.build(app) }
end
# Locks the middleware stack to ensure no further modifications are possible.
def lock!
@handlers.freeze
end
def locked?
@handlers.frozen?
end
def use(klass, *args)
raise_if_locked
block = block_given? ? Proc.new : nil
@handlers << self.class::Handler.new(klass, *args, &block)
end
@ -99,6 +113,7 @@ module Faraday
## methods to push onto the various positions in the stack:
def insert(index, *args, &block)
raise_if_locked
index = assert_index(index)
handler = self.class::Handler.new(*args, &block)
@handlers.insert(index, handler)
@ -112,17 +127,23 @@ module Faraday
end
def swap(index, *args, &block)
raise_if_locked
index = assert_index(index)
@handlers.delete_at(index)
insert(index, *args, &block)
end
def delete(handler)
raise_if_locked
@handlers.delete(handler)
end
private
def raise_if_locked
raise StackLocked, "can't modify middleware stack after making a request" if locked?
end
def use_symbol(mod, key, *args)
block = block_given? ? Proc.new : nil
use(mod.lookup_module(key), *args, &block)

View File

@ -61,6 +61,25 @@ module Faraday
@builder.build(options, &block)
end
# The "rack app" wrapped in middleware. All requests are sent here.
#
# The builder is responsible for creating the app object. After this,
# the builder gets locked to ensure no further modifications are made
# to the middleware stack.
#
# Returns an object that responds to `call` and returns a Response.
def app
@app ||= begin
builder.lock!
builder.to_app(lambda { |env|
# the inner app that creates and returns the Response object
response = Response.new
response.finish(env) unless env[:parallel_manager]
env[:response] = response
})
end
end
def get(url = nil, headers = nil)
block = block_given? ? Proc.new : nil
run_request(:get, url, nil, headers, &block)

View File

@ -77,13 +77,8 @@ module Faraday
end
def run(connection, request_method)
app = lambda { |env|
response = Response.new
response.finish(env) unless env[:parallel_manager]
env[:response] = response
}
env = to_env_hash(connection, request_method)
connection.builder.to_app(app).call(env)
connection.app.call(env)
end
end
end

View File

@ -158,19 +158,19 @@ else
def create_connection(adapter)
if adapter == :default
Faraday.default_connection.tap do |conn|
conn.url_prefix = LIVE_SERVER
conn.headers['X-Faraday-Adapter'] = adapter.to_s
end
builder_block = nil
else
Faraday::Connection.new LIVE_SERVER, :headers => {'X-Faraday-Adapter' => adapter.to_s} do |b|
builder_block = Proc.new do |b|
b.request :multipart
b.request :url_encoded
b.use adapter
end
end.tap do |conn|
target = conn.builder.handlers.last
conn.builder.insert_before target, Faraday::Response::RaiseError
end
Faraday::Connection.new(LIVE_SERVER, &builder_block).tap do |conn|
conn.headers['X-Faraday-Adapter'] = adapter.to_s
adapter_handler = conn.builder.handlers.last
conn.builder.insert_before adapter_handler, Faraday::Response::RaiseError
end
end

View File

@ -26,8 +26,6 @@ class MiddlewareStackTest < Faraday::TestCase
def test_allows_rebuilding
build_stack Apple
assert_handlers %w[Apple]
build_stack Orange
assert_handlers %w[Orange]
end
@ -67,6 +65,28 @@ class MiddlewareStackTest < Faraday::TestCase
assert_handlers %w[Orange]
end
def test_stack_is_locked_after_making_requests
build_stack Apple
assert !@builder.locked?
@conn.get('/')
assert @builder.locked?
assert_raises Faraday::Builder::StackLocked do
@conn.use Orange
end
end
def test_duped_stack_is_unlocked
build_stack Apple
assert !@builder.locked?
@builder.lock!
assert @builder.locked?
duped_connection = @conn.dup
assert_equal @builder, duped_connection.builder
assert !duped_connection.builder.locked?
end
private
# make a stack with test adapter that reflects the order of middleware