Skip to content

Commit 902f99c

Browse files
hdimersloria
andauthored
Fix URL validator rejecting a fragment after an empty path (#3016)
* Fix URL validator rejecting a fragment after an empty path `validate.URL` rejected absolute URLs whose fragment follows an empty path with no query, such as `https://example.com#frag`. RFC 3986 permits a fragment after an empty path, and anchor links to a domain root are common in practice. Allow `#` to introduce the URL tail so the fragment is accepted. This also makes a bare `#frag` valid in relative mode, matching the existing handling of a bare `?query`. * Add changelog entry for URL fragment fix (:pr:`3016`) * Remove explanatory comment per review * credit --------- Co-authored-by: Steven Loria <git@stevenloria.com>
1 parent 3207be3 commit 902f99c

4 files changed

Lines changed: 11 additions & 1 deletion

File tree

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,4 @@ Contributors (chronological)
188188
- `@rstar327 <https://github.com/rstar327>`_
189189
- Kadir Can Ozden `@bysiber <https://github.com/bysiber>`_
190190
- Dhruvil Darji `@dhruvildarji <https://github.com/dhruvildarji>`_
191+
- Haïm Dimer `@hdimer <https://github.com/hdimer>`_

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Features:
99
- If `by_value` is enabled on an enum with a `None` value `allow_none` now defaults to `True`.
1010
Thanks :user:`GeraldineGalindo` for the suggestion (:issue:`2985`).
1111

12+
Bug fixes:
13+
14+
- `marshmallow.validate.URL` accepts a fragment that follows an empty path,
15+
e.g. ``https://example.com#frag`` (:pr:`3016`). Thanks :user:`hdimer` for the PR.
16+
1217
4.3.0 (2026-04-03)
1318
------------------
1419

src/marshmallow/validate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def _regex_generator(
157157
r"(?::\d+)?",
158158
)
159159
)
160-
relative_part = r"(?:/?|[/?]\S+)\Z"
160+
relative_part = r"(?:/?|[/?#]\S+)\Z"
161161

162162
if relative:
163163
if absolute:

tests/test_validate.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"http://:pass@example.com",
3333
"http://@example.com",
3434
"http://AZaz09-._~%2A!$&'()*+,;=:@example.com",
35+
"http://example.org#fragment",
36+
"http://user@example.com#fragment",
3537
],
3638
)
3739
def test_url_absolute_valid(valid_url):
@@ -88,6 +90,7 @@ def test_url_absolute_invalid(invalid_url):
8890
"/foo/bar",
8991
"/foo?bar",
9092
"/foo?bar#baz",
93+
"#frag",
9194
],
9295
)
9396
def test_url_relative_valid(valid_url):
@@ -124,6 +127,7 @@ def test_url_relative_invalid(invalid_url):
124127
"/foo?bar",
125128
"?bar",
126129
"/foo?bar#baz",
130+
"#frag",
127131
],
128132
)
129133
def test_url_relative_only_valid(valid_url):

0 commit comments

Comments
 (0)