Skip to content

Fix fragment with path structure misparsed as a query (#172)#189

Open
gaoflow wants to merge 1 commit into
gruns:masterfrom
gaoflow:fix-fragment-path-with-equals
Open

Fix fragment with path structure misparsed as a query (#172)#189
gaoflow wants to merge 1 commit into
gruns:masterfrom
gaoflow:fix-fragment-path-with-equals

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 20, 2026

Copy link
Copy Markdown

Fixes #172.

Summary

A URL fragment that contains path structure is silently misparsed as a query, corrupting its /:

>>> import furl
>>> str(furl.furl('https://example.com/path/#/foo=123'))
'https://example.com/path/#%2Ffoo=123'   # expected '…#/foo=123'

The / is percent-encoded to %2F — exactly the round-trip corruption reported in #172.

Cause

In Fragment.load, a fragment with no ? was routed to the query parser whenever it contained an =:

if '=' in fragment:  # Query example: '#woofs=dogs'.
    self._query.load(fragment)
else:
    self._path.load(fragment)

So #/foo=123 was treated as a query, making /foo a query key whose / is then percent-encoded on serialization. The branch had no test coverage for =-without-? inputs that also contain /.

Fix

Only treat such a fragment as a query when = precedes any / (or there is no / at all). A / before the first = signals path structure:

if '=' in fragment and (
    '/' not in fragment or fragment.find('=') < fragment.find('/')
):
    self._query.load(fragment)
else:
    self._path.load(fragment)
  • #woofs=dogs → query (unchanged)
  • #key=val/ue → query (unchanged — the / is in the value, after =)
  • #/foo=123, #a/b=c, #/a=1&b=2 → path (the / is preserved)
  • The ?-split branch (path + query) is untouched.

Verification

  • Added regression cases to TestFragment.test_load (the /-before-= path cases, plus a =-before-/ query case to pin the unchanged behavior). They fail before the fix (the fragment parses as a query with an empty path) and pass after.
  • Full suite: 70 passed, 3 failed — the 3 failures (test_hosts/test_netloc/test_odd_urls) are pre-existing ipaddress/urllib version drift, identical with and without this change (baseline is 69 passed / 4 failed; my new assertions account for the one-test delta).
  • flake8 clean.

This pull request was prepared with the assistance of AI, under my direction and review.

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.
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.

URL fragment percent-encoding

1 participant