lostisland/faraday · error · Faraday::RackBuilder::StackLocked
can't modify middleware stack after making a request
Error message
can't modify middleware stack after making a request
What it means
Faraday::RackBuilder freezes its handler list via lock! before the first request is executed; locked? then returns true (the handlers array is frozen). Every mutation method (use, insert, insert_before, insert_after, swap, delete, and the Connection helpers request/response/adapter that delegate to them) first calls raise_if_locked, which raises StackLocked with 'can't modify middleware stack after making a request'. The design makes a connection's middleware stack immutable once it has served traffic, because the built app closure is already fixed.
Source
Thrown at lib/faraday/rack_builder.rb:220
# :uri - Proxy Server URI
# :user - Proxy server username
# :password - Proxy server password
# :ssl - Hash of options for configuring SSL requests.
def build_env(connection, request)
exclusive_url = connection.build_exclusive_url(
request.path, request.params,
request.options.params_encoder
)
Env.new(request.http_method, request.body, exclusive_url,
request.options, request.headers, connection.ssl,
connection.parallel_manager)
end
private
def raise_if_locked
raise StackLocked, LOCK_ERR if locked?
end
def raise_if_adapter(klass)
return unless klass <= Faraday::Adapter
raise 'Adapter should be set using the `adapter` method, not `use`'
end
def ensure_adapter!
raise MISSING_ADAPTER_ERROR unless @adapter
end
def adapter_set?
!@adapter.nil?
end
def use_symbol(mod, key, ...)
use(mod.lookup_middleware(key), ...)View on GitHub (pinned to b25b1b26cc)
Solutions
- Move all middleware configuration into the Faraday.new block (or before the first request) so the stack is complete before it locks.
- When you need a different stack after requests have run, build a new connection: conn = Faraday.new(url) { |b| ... } — connections are cheap to create.
- Express per-request variability through middleware options or per-request req.options/headers instead of editing the stack.
- In tests, either create a fresh connection per example or stub/reset via a new Stubs/adapter instead of mutating the shared one.
Example fix
# before
conn = Faraday.new('https://api.example.com')
conn.get('/health') # first request locks the stack
conn.response :json # => Faraday::RackBuilder::StackLocked
# after
conn = Faraday.new('https://api.example.com') do |b|
b.response :json # configure before any request
b.adapter :net_http
end
conn.get('/health') Defensive patterns
Strategy: validation
Validate before calling
raise 'stack locked; build a new connection' if conn.builder.locked? conn.use MyMiddleware
Type guard
def stack_mutable?(conn) !conn.builder.locked? end
Try / catch
begin
conn.response :json
rescue Faraday::RackBuilder::StackLocked
conn = Faraday.new(conn.url_prefix.to_s) { |b| b.response :json; b.adapter :net_http }
end Prevention
- Put all middleware setup in the Faraday.new block; treat the stack as immutable after creation.
- Need a different stack later? Create a new connection — never edit a warmed one.
- Model per-request variability as middleware options / per-request req options, not stack mutations.
- In test suites, build a fresh connection per example instead of mutating a shared one.
When it happens
Trigger: Calling conn.use MyMiddleware, conn.response :json, conn.request :retry, or conn.builder.delete(...) after any request has already run through that connection; test fixtures that make a warm-up/sanity request and then register instrumentation middleware; connection objects stored in constants or memoized service objects that get 'upgraded' mid-lifecycle.
Common situations: Long-lived connections in service objects where configuration code runs lazily on first use and middleware registration happens after a boot-time health check request; test suites that swap middleware per example while sharing one connection; code that tries to add an auth middleware right after the first request revealed a 401.
Related errors
- Unexpected params received (got #{params.size} instead of 1)
- Expected :read, :write, :open. Got #{type.inspect} :(
- no stubbed request for #{env[:method]} #{env[:url]} #{env[:b
- unknown http method: #{method}
- Can't convert #{params.class} into Hash.
AI-assisted analysis of lostisland/faraday@b25b1b26cc (2026-08-21).
Data as JSON: /api/errors/05ace38c199e4e3b.
Report an issue: GitHub.