Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/crawlee/_utils/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ def normalize_url(url: str, *, keep_url_fragment: bool = False) -> str:
This function cleans and standardizes a URL by removing leading and trailing whitespaces,
converting the scheme and netloc to lower case, stripping unwanted tracking parameters
(specifically those beginning with 'utm_'), sorting the remaining query parameters alphabetically,
and optionally retaining the URL fragment. The goal is to ensure that URLs that are functionally
identical but differ in trivial ways (such as parameter order or casing) are treated as the same.
and optionally retaining the URL fragment. URL paths, queries, and fragments keep their original
casing, since only the scheme and the host are case-insensitive (RFC 3986, section 6.2.2.1).
The goal is to ensure that URLs that are functionally
identical but differ in trivial ways (such as parameter order or scheme/host casing) are treated as the same.

Args:
url: The URL to be normalized.
Expand All @@ -44,7 +46,11 @@ def normalize_url(url: str, *, keep_url_fragment: bool = False) -> str:
yarl_new_url.path.removesuffix('/'), keep_query=True, keep_fragment=keep_url_fragment
)

return str(yarl_new_url).lower()
# Only the scheme and the host are case-insensitive (RFC 3986, section 6.2.2.1), and yarl has
# already lowercased both during parsing. The path, query, and fragment must keep their original
# casing, since lowercasing them can silently merge distinct URLs (e.g. case-sensitive paths or
# tokens) during deduplication.
return str(yarl_new_url)


def compute_unique_key(
Expand Down
8 changes: 6 additions & 2 deletions tests/unit/_utils/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,26 @@
'http://example.com/?another_key=another_value&key=value',
False,
),
('HTTPS://EXAMPLE.COM/?KEY=VALUE', 'https://example.com/?key=value', False),
('HTTPS://EXAMPLE.COM/?KEY=VALUE', 'https://example.com/?KEY=VALUE', False),
('', '', False),
('http://example.com/#fragment', 'http://example.com/#fragment', True),
('http://example.com/#fragment', 'http://example.com', False),
(' https://example.com/ ', 'https://example.com', False),
('http://example.com/?b=2&a=1', 'http://example.com/?a=1&b=2', False),
('https://example.com/Product/ABC', 'https://example.com/Product/ABC', False),
('https://example.com/?token=SeCrEt', 'https://example.com/?token=SeCrEt', False),
],
ids=[
'remove_utm_params',
'retain_sort_non_utm_params',
'convert_scheme_netloc_to_lowercase',
'convert_scheme_netloc_to_lowercase_only',
'handle_empty_url',
'retain_fragment',
'remove_fragment',
'trim_whitespace',
'sort_query_params',
'preserve_path_casing',
'preserve_query_casing',
],
)
def test_normalize_url(url: str, expected_output: str, *, keep_url_fragment: bool) -> None:
Expand Down