From ed2d48a3625c41dbec9c5418b66bd5a686d40071 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Sat, 20 Jun 2026 18:58:59 +0200 Subject: [PATCH] Fix fragment with path structure misparsed as a query (#172) A fragment with no '?' was treated as a query whenever it contained '=', so a fragment like '#/foo=123' was parsed as a query and its '/' was percent-encoded, round-tripping to '#%2Ffoo=123' instead of '#/foo=123'. Only treat such a fragment as a query when '=' precedes any '/' (or there is no '/'); a '/' before the first '=' signals path structure. Fragments like '#woofs=dogs' stay queries; '#/foo=123' and '#a/b=c' are now paths. --- furl/furl.py | 13 +++++++++++-- tests/test_furl.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/furl/furl.py b/furl/furl.py index cd6d710..183c848 100644 --- a/furl/furl.py +++ b/furl/furl.py @@ -1213,9 +1213,18 @@ def load(self, fragment): elif len(toks) == 1: # Does this fragment look like a path or a query? Default to # path. - if '=' in fragment: # Query example: '#woofs=dogs'. + # + # A '=' alone isn't enough to call it a query: a fragment like + # '#/foo=123' is a path that happens to contain '=' (and its '/' + # would wrongly get percent-encoded if treated as a query key). + # Only treat it as a query when '=' comes before any '/', or no + # '/' is present at all. A '/' before the first '=' signals path + # structure. See issue #172. + if '=' in fragment and ( + '/' not in fragment or fragment.find('=') < fragment.find('/') + ): # Query example: '#woofs=dogs'. self._query.load(fragment) - else: # Path example: '#supinthisthread'. + else: # Path example: '#supinthisthread', '#/foo=123'. self._path.load(fragment) else: # Does toks[1] actually look like a query? Like 'a=a' or diff --git a/tests/test_furl.py b/tests/test_furl.py index bc268c8..d223136 100644 --- a/tests/test_furl.py +++ b/tests/test_furl.py @@ -989,6 +989,16 @@ def test_load(self): {'a': 'a', 'hok sprm': None}), ('/sch/toot?a=a&hok sprm', '/sch/toot', {'a': 'a', 'hok sprm': None}), + # A '=' without a '?' is only a query when '=' precedes any + # '/'. A '/' before the first '=' means the fragment is a + # path that merely contains '=' and its '/' must not be + # percent-encoded as a query key would be (issue #172). + ('/foo=123', '/foo=123', {}), + ('a/b=c', 'a/b=c', {}), + ('/a=1&b=2', '/a=1&b=2', {}), + ('/foo=123/bar=456', '/foo=123/bar=456', {}), + # '=' before any '/' still parses as a query (unchanged). + ('woofs=dogs', '', {'woofs': 'dogs'}), ] for fragment, path, query in comps: