Skip to content

Fix FrozenError when passing a frozen URI as the base URL - #1685

Open
Leonard013 wants to merge 1 commit into
lostisland:mainfrom
Leonard013:fix/1349-frozen-uri-base-url
Open

Fix FrozenError when passing a frozen URI as the base URL#1685
Leonard013 wants to merge 1 commit into
lostisland:mainfrom
Leonard013:fix/1349-frozen-uri-base-url

Conversation

@Leonard013

@Leonard013 Leonard013 commented Jul 18, 2026

Copy link
Copy Markdown

Fix FrozenError when passing a frozen URI as the base URL

Fixes #1349

Problem

Passing a frozen URI (or, more generally, any URI instance the caller
still holds a reference to) as the base URL to Faraday.new crashes:

endpoint = URI('http://example.org/api/v1').freeze
Faraday.new(endpoint)
# => FrozenError: can't modify frozen URI::HTTP: #<URI::HTTP http://example.org/api/v1>

Even without freeze, the connection mutates the exact URI object the
caller passed in, which is a surprising side effect.

Root cause

Faraday::Utils.URI (lib/faraday/utils.rb:70-78) returns the argument
untouched whenever it already responds to :host, i.e. it never duplicates an
incoming URI:

def URI(url)
  if url.respond_to?(:host)
    url            # <- returned as-is, no dup
  elsif url.respond_to?(:to_str)
    default_uri_parser.call(url)
  else
    raise ArgumentError, 'bad argument (expected URI object or URI string)'
  end
end

Connection#url_prefix= (lib/faraday/connection.rb:365) then mutates that
exact object in place while pulling apart the URL:

  • self.path_prefix = uri.pathurl_prefix.path = ... (connection.rb:391)
  • uri.query = nil (connection.rb:369)
  • uri.user = uri.password = nil (connection.rb:373)

The first of these (path=) is enough to raise FrozenError on a frozen URI;
all three would mutate a caller-owned URI in place.

Fix

Coerce the incoming value to a String before handing it to Utils.URI in
Connection#url_prefix=, so Utils.URI always parses a brand-new, mutable
URI instance rather than returning (and then mutating) the caller's object:

uri = @url_prefix = Utils.URI(url.to_s)

This follows the maintainer's suggested direction on the issue (prefer the
simple #dup/.to_s approach, with a comment explaining why). A String base
URL — the common case — is unaffected: it was already re-parsed before, and
still is. The change is scoped to url_prefix=; the trailing
proxy_from_env(url) call still receives the original url, so proxy handling
for URI inputs is unchanged.

Tests

Added a regression example under Faraday::Connection.new in
spec/faraday/connection_spec.rb that passes a frozen URI carrying both a
query string and basic-auth credentials, exercising all three mutation sites
(path=, query=, user=/password=):

context 'with a frozen URI as url' do
  let(:url) { URI('http://Aladdin:open%20sesame@httpbingo.org/fish?a=1').freeze }

  it { expect { subject }.not_to raise_error }
  it { expect(subject.url_prefix.to_s).to eq('http://httpbingo.org/fish') }
  it { expect(subject.params).to eq('a' => '1') }
  it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') }
end

Verification (before / after)

Before the fix, the new examples fail:

$ bundle exec rspec spec/faraday/connection_spec.rb -e 'with a frozen URI as url'
4 examples, 4 failures
# FrozenError: can't modify frozen URI::HTTP ... connection.rb:391:in 'path_prefix='

After the fix:

$ bundle exec rspec spec/faraday/connection_spec.rb -e 'with a frozen URI as url'
4 examples, 0 failures

$ bundle exec rspec spec/faraday/connection_spec.rb
142 examples, 0 failures

$ bundle exec rspec
643 examples, 0 failures

$ bundle exec rubocop lib/faraday/connection.rb spec/faraday/connection_spec.rb
2 files inspected, no offenses detected

Compatibility

No breaking changes. Behaviour for String base URLs is identical. URI base
URLs now work when frozen and are no longer mutated in place — strictly a
bug fix. Public API and method signatures are unchanged.

Faraday::Utils.URI returns an incoming URI untouched (no dup) whenever it
responds to :host, and Connection#url_prefix= then mutates that exact object
in place (path=, query=, user=/password=). Passing a frozen URI therefore
raised "FrozenError: can't modify frozen URI::HTTP", and passing an unfrozen
URI silently mutated the caller's object.

Coerce the value to a String before handing it to Utils.URI in url_prefix=,
so a brand-new, mutable URI is always parsed. String base URLs are unaffected.

Fixes lostisland#1349

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NX8ygWTRa86kE8k8Yn9bpu
Comment thread lib/faraday/connection.rb
# URI instance. Otherwise, when passed an existing URI, Utils.URI returns
# it untouched and the in-place mutations below (`path=`, `query=`,
# `user=`/`password=`) would either raise a FrozenError on a frozen URI
# or unexpectedly modify the caller's object.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop this implementation level comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Faraday tries to modify frozen URI (FrozenError)

2 participants