Skip to content

Add IPv6 scope ID handling in Uri and related tests - #135617

Open
makazeu wants to merge 3 commits into
dotnet:mainfrom
makazeu:fix-uri-ipv6-scope
Open

makazeu wants to merge 3 commits into
dotnet:mainfrom
makazeu:fix-uri-ipv6-scope

Conversation

@makazeu

@makazeu makazeu commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Uri.Equals and Uri.GetHashCode use a cached representation based on UriComponents.HttpRequestUrl, which omits IPv6 scope IDs. Consequently, URIs such as http://[fe80::1%1]/ and http://[fe80::1%2]/ can compare equal despite identifying different local network zones.

This affects consumers that use Uri as a key. In SocketsHttpHandler, changing a proxy's IPv6 scope can reuse the previous proxy's connection pool and select the wrong connection endpoint. This change fixes URI identity at the source rather than introducing a handler-specific workaround.

Approach

  • Compare IPv6 scope IDs separately and include them in the hash for simple parsers, including built-in parsers and directly registered standard parser types.
  • Compare scope text ordinally, distinguishing different scopes and an absent scope from %0, without resolving interface names or normalizing scope values.
  • Prevent the case-insensitive file-URI raw comparison shortcut from treating differently cased scope IDs as equal, while preserving existing file-path comparison behavior.
  • Preserve custom-parser behavior. Custom parsers can supply HttpRequestUrl without initializing the host or scope cache; including that lazy cache could change a URI's hash after accessing host properties.

HttpRequestUrl continues to omit scope IDs. Scope identifies a local network zone and is needed for connection selection, but should not be transmitted as part of an outgoing HTTP URI. The fix separates local URI identity from that request representation.

Testing

Local validation used Linux x64 through WSL, with Debug library/test builds and the existing Release CoreCLR runtime.

Validation Result
System.Private.Uri functional inner-loop suite 15,320 passed
System.Private.Uri unit suite 19 passed
Targeted parser tests after restoring the existing recovering-parser test unchanged 12 passed
HTTP proxy and connection-key tests, including HTTP, HTTPS, and SOCKS5 proxy regressions 38 passed; 1 existing IsWinHttpHandler-specific skip

URI coverage includes scope differences, absent scope versus %0, equivalent IPv6 representations, file URI semantics, disabled path/query canonicalization, registered simple parsers, and custom-parser equality/hash stability.

The proxy regressions verify that changing scope selects the new proxy endpoint. The HTTPS case checks endpoint selection before TLS, not a completed TLS handshake. All three proxy cases fail against the original Uri implementation, selecting %1 when %2 is expected. The focused URI run against the original implementation also produced seven expected failures.

Resolves #135103

Note

This description was generated with GitHub Copilot.

@makazeu
makazeu marked this pull request as ready for review October 12, 2026 08:19
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

1 similar comment
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 4 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Oct 12, 2026
{
// Try raw compare for _strings as the last chance to keep the working set small
if (string.Equals(_string, other._string, IsUncOrDosPath ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
// Simple IPv6 parsers must keep scope IDs case-sensitive, even for file URIs.

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.

Do we have tests for all the added IsSimple checks?

@makazeu makazeu Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The hash and final scope-comparison checks are covered by the simple-parser tests and the custom/custom and custom/simple comparisons, including hash stability after accessing IdnHost. However, the non-simple file-style case for the raw-string shortcut is missing. I propose adding a focused test for that branch.

Update:
On closer inspection, the raw-string shortcut's IsSimple check is redundant: FileStyleUriParser, including derived types, inherits SimpleUserSyntax from the built-in file parser. I removed that check. The file-URI cases added to the existing equality test cover the raw-string shortcut, and the existing simple-parser and custom-parser tests cover the remaining IsSimple checks.

Comment thread src/libraries/System.Private.Uri/src/System/Uri.cs
Comment on lines +1557 to +1559
// Custom parsers define identity through HttpRequestUrl and may return it without
// initializing the host. ScopeId can remain null until Host or IdnHost is accessed,
// so including it could change the hash after those properties populate the cache.

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.

Should we ensure it's initialized in that case?

@makazeu makazeu Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ensuring initialization consistently would address the lazy-cache issue. My reason for the IsSimple guard is compatibility: for a non-simple parser, forcing host initialization via EnsureHostString(false) can invoke both Host and StrongPort callbacks and trigger additional validation.

Including the initialized scope would also change custom-parser equality, rather than merely stabilize the cache. The current patch preserves that behavior, though it also leaves non-simple derived parsers that simply delegate to the base implementation unchanged. I would prefer to retain that compatibility boundary unless we intend to extend the scope change to non-simple parsers as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The relevant code path is that IsSimple selects GetComponentsHelper, whereas non-simple parsers dispatch to the overridable GetComponents implementation. Since HttpRequestUrl includes Host, the standard formatter calls EnsureHostString. For a scoped IPv6 host, this initializes the host and scope as part of generating the comparison/hash representation. A custom override can return that representation without taking this path.

Note

This reply was drafted with GitHub Copilot.

Comment thread src/libraries/System.Private.Uri/tests/FunctionalTests/UriMethodsTests.cs Outdated

This branch has not been deployed

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

Labels

area-System.Net community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Inconsistent handling of IPv6 scope in SocketsHttpHandler's connection pool

2 participants