dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.61k stars 4.56k forks source link

Uri.MakeRelativeUri ignores query of base URI #89708

Open KalleOlaviNiemitalo opened 11 months ago

KalleOlaviNiemitalo commented 11 months ago

Description

Uri.MakeRelativeUri returns the wrong relative URI if the base URI has a query and the argument URI has the same scheme, authority, and path but no query. When the base URI and the relative URI are combined using the Uri(Uri, Uri) constructor, the result is not equal to the original argument URI.

Reproduction Steps

# This is compatible with PowerShell 2.0, too.
function Demo ([Uri]$Base, [Uri]$Target) {
    $Relative = $Base.MakeRelativeUri($Target)
    $Combined = New-Object "System.Uri" -ArgumentList @($Base, $Relative)
    [PSCustomObject]@{
        Base = $Base
        Target = $Target
        Relative = $Relative
        Combined = $Combined
        Match = $Combined -eq $Target
    }
}

$Base1 = ([Uri]"http://localhost/alpha#gamma")
$Base2 = ([Uri]"http://localhost/alpha?beta#gamma")
$Target1 = ([Uri]"http://localhost/alpha#delta")
$Target2 = ([Uri]"http://localhost/alpha?beta#delta")

@(
    Demo $Base1 $Target1
    Demo $Base1 $Target2
    Demo $Base2 $Target1
    Demo $Base2 $Target2
) | Format-Table

Expected behavior

Base                              Target                            Relative    Combined                          Match
----                              ------                            --------    --------                          -----
http://localhost/alpha#gamma      http://localhost/alpha#delta      #delta      http://localhost/alpha#delta       True
http://localhost/alpha#gamma      http://localhost/alpha?beta#delta ?beta#delta http://localhost/alpha?beta#delta  True
http://localhost/alpha?beta#gamma http://localhost/alpha#delta      alpha#delta http://localhost/alpha#delta       True
http://localhost/alpha?beta#gamma http://localhost/alpha?beta#delta #delta      http://localhost/alpha?beta#delta  True

Actual behavior

Base                              Target                            Relative    Combined                          Match
----                              ------                            --------    --------                          -----
http://localhost/alpha#gamma      http://localhost/alpha#delta      #delta      http://localhost/alpha#delta       True
http://localhost/alpha#gamma      http://localhost/alpha?beta#delta ?beta#delta http://localhost/alpha?beta#delta  True
http://localhost/alpha?beta#gamma http://localhost/alpha#delta      #delta      http://localhost/alpha?beta#delta False
http://localhost/alpha?beta#gamma http://localhost/alpha?beta#delta ?beta#delta http://localhost/alpha?beta#delta  True

Regression?

No, I get the same incorrect result on .NET Framework.

Known Workarounds

No response

Configuration

PowerShell 7.3.5 using .NET 7.0.8 on Windows 10 version 22H2 x64.

Other information

No response

ghost commented 11 months ago

Tagging subscribers to this area: @dotnet/ncl See info in area-owners.md if you want to be subscribed.

Issue Details
### Description Uri.MakeRelativeUri returns the wrong relative URI if the base URI has a query and the argument URI has the same scheme, authority, and path but no query. When the base URI and the relative URI are combined using the Uri(Uri, Uri) constructor, the result is not equal to the original argument URI. ### Reproduction Steps ```PowerShell # This is compatible with PowerShell 2.0, too. function Demo ([Uri]$Base, [Uri]$Target) { $Relative = $Base.MakeRelativeUri($Target) $Combined = New-Object "System.Uri" -ArgumentList @($Base, $Relative) [PSCustomObject]@{ Base = $Base Target = $Target Relative = $Relative Combined = $Combined Match = $Combined -eq $Target } } $Base1 = ([Uri]"http://localhost/alpha#gamma") $Base2 = ([Uri]"http://localhost/alpha?beta#gamma") $Target1 = ([Uri]"http://localhost/alpha#delta") $Target2 = ([Uri]"http://localhost/alpha?beta#delta") @( Demo $Base1 $Target1 Demo $Base1 $Target2 Demo $Base2 $Target1 Demo $Base2 $Target2 ) | Format-Table ``` ### Expected behavior ```none Base Target Relative Combined Match ---- ------ -------- -------- ----- http://localhost/alpha#gamma http://localhost/alpha#delta #delta http://localhost/alpha#delta True http://localhost/alpha#gamma http://localhost/alpha?beta#delta ?beta#delta http://localhost/alpha?beta#delta True http://localhost/alpha?beta#gamma http://localhost/alpha#delta alpha#delta http://localhost/alpha#delta True http://localhost/alpha?beta#gamma http://localhost/alpha?beta#delta #delta http://localhost/alpha?beta#delta True ``` ### Actual behavior ```none Base Target Relative Combined Match ---- ------ -------- -------- ----- http://localhost/alpha#gamma http://localhost/alpha#delta #delta http://localhost/alpha#delta True http://localhost/alpha#gamma http://localhost/alpha?beta#delta ?beta#delta http://localhost/alpha?beta#delta True http://localhost/alpha?beta#gamma http://localhost/alpha#delta #delta http://localhost/alpha?beta#delta False http://localhost/alpha?beta#gamma http://localhost/alpha?beta#delta ?beta#delta http://localhost/alpha?beta#delta True ``` ### Regression? No, I get the same incorrect result on .NET Framework. ### Known Workarounds _No response_ ### Configuration PowerShell 7.3.5 using .NET 7.0.8 on Windows 10 version 22H2 x64. ### Other information _No response_
Author: KalleOlaviNiemitalo
Assignees: -
Labels: `area-System.Net`
Milestone: -
karelz commented 11 months ago

It boils down to the fact, that empty relative Uri will keep query and fragment part of the base Uri when combining. (@MihaZupan will take a look at the spec if this is correct behavior) While query and fragment of the base are ignored in MakeRelativeUri call, which makes sense.

In isolation both cases make sense to me, which would make it By Design. Let's see what @MihaZupan finds in the RFC.

Here's C# code:

    static void Main()
    {
        Uri baseUri = new Uri("http://localhost/alpha?beta#gamma");
        Uri targetUri = new Uri("http://localhost/alpha");
        string relative1 = "";
        string relative2 = "#hello";
        string relative3 = "?world";

        PrintRelative(baseUri, targetUri);
        PrintCombined(baseUri, relative1);
        PrintCombined(baseUri, relative2);
        PrintCombined(baseUri, relative3);
    }
    static void PrintRelative(Uri baseUri, Uri targetUri)
    {
        Console.WriteLine($"Base:     {baseUri}");
        Console.WriteLine($"Target:   {targetUri}");
        Uri relativeUri = baseUri.MakeRelativeUri(targetUri);
        Console.WriteLine($"Relative: {relativeUri}");
        Console.WriteLine();
    }
    static void PrintCombined(Uri baseUri, string relativeUri)
    {
        Console.WriteLine($"Base:     {baseUri}");
        Console.WriteLine($"Relative: {relativeUri}");
        Uri combinedUri = new Uri(baseUri, relativeUri);
        Console.WriteLine($"Combined: {combinedUri}");
        Console.WriteLine();
    }

Output:

Base:     http://localhost/alpha?beta#gamma
Target:   http://localhost/alpha
Relative:

Base:     http://localhost/alpha?beta#gamma
Relative:
Combined: http://localhost/alpha?beta#gamma

Base:     http://localhost/alpha?beta#gamma
Relative: #hello
Combined: http://localhost/alpha?beta#hello

Base:     http://localhost/alpha?beta#gamma
Relative: ?world
Combined: http://localhost/alpha?world
KalleOlaviNiemitalo commented 11 months ago

AFAIK, the Uri(Uri, Uri) constructor complies with IETF RFC 3986 section 5.2 Relative Resolution. That RFC does not define an algorithm for Uri.MakeRelativeUri to use. Anyway, Uri.MakeRelativeUri is documented as returning "a relative Uri that, when appended to the current URI instance, yields uri". If "appended" means the Uri(Uri, Uri) constructor, then Uri.MakeRelativeUri does not work as documented.

karelz commented 11 months ago

Anyway, Uri.MakeRelativeUri is documented as returning "a relative Uri that, when appended to the current URI instance, yields uri"

Which it does, except the case of empty relative path and base Uri having query or fragment. I am not surprised such corner case was overlooked in the documentation.

MihaZupan commented 11 months ago

The way I read something like

baseUri.MakeRelativeUri(targetUri);

is along the lines of "what should I put into an href so that a browser currently at {baseUri} will navigate to {targetUri}". That would also imply that the following holds:

targetUri == new Uri(baseUri, baseUri.MakeRelativeUri(targetUri)))

It looks like this breaks down if paths are the same and base has a query and target doesn't have a query.

With that, I would expect that MakeRelativeUri would take the merging behavior into account, such that the following expected behavior would make sense to me

Base                              Target                            Relative
----                              ------                            --------
Expected:
http://localhost/alpha?beta#gamma http://localhost/alpha#delta      alpha#delta

Actual:
http://localhost/alpha?beta#gamma http://localhost/alpha#delta      #delta

The other case that was pointed out is fine IMO, given that both will produce the same result when merged.

Expected:
http://localhost/alpha?beta#gamma http://localhost/alpha?beta#delta #delta

Actual (but still fine):
http://localhost/alpha?beta#gamma http://localhost/alpha?beta#delta ?beta#delta

I'm inclined to say that this is a bug, though I don't have a good sense of how impactful of a breaking change fixing it would be.

karelz commented 11 months ago

@MihaZupan I don't understand why in your first case you expect relative to be "alpha#delta". I would expect it to be "#delta", which is actual behavior. What am I missing?

karelz commented 11 months ago

Triage: Even if we decide something is a bug, we should keep in mind that diverging from .NET Framework might not be desirable for some SW. Unless it is really problematic, we should likely Won't Fix it. Moving to 9.0 to make a decision there.