diff --git a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
index 2d13b0ccb7fc..9c4ab4216a93 100644
--- a/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
+++ b/src/Components/Components/src/Microsoft.AspNetCore.Components.csproj
@@ -19,10 +19,11 @@
+
-
+
diff --git a/src/Components/Components/src/Routing/RouteContext.cs b/src/Components/Components/src/Routing/RouteContext.cs
index de7aeedb7e5d..bb2739887103 100644
--- a/src/Components/Components/src/Routing/RouteContext.cs
+++ b/src/Components/Components/src/Routing/RouteContext.cs
@@ -1,7 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Buffers;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Text;
+using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.Routing.Tree;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
@@ -11,7 +15,28 @@ internal sealed class RouteContext
{
public RouteContext(string path)
{
- Path = Uri.UnescapeDataString(path);
+ Path = path.Contains('%') ? GetDecodedPath(path) : path;
+
+ [SkipLocalsInit]
+ static string GetDecodedPath(string path)
+ {
+ using var uriBuffer = path.Length < 128 ?
+ new UriBuffer(stackalloc byte[path.Length]) :
+ new UriBuffer(path.Length);
+
+ var utf8Span = uriBuffer.Buffer;
+
+ if (Encoding.UTF8.TryGetBytes(path.AsSpan(), utf8Span, out var written))
+ {
+ utf8Span = utf8Span[..written];
+ var decodedLength = UrlDecoder.DecodeInPlace(utf8Span, isFormEncoding: false);
+ utf8Span = utf8Span[..decodedLength];
+ path = Encoding.UTF8.GetString(utf8Span);
+ return path;
+ }
+
+ return path;
+ }
}
public string Path { get; set; }
@@ -24,4 +49,27 @@ public RouteContext(string path)
public Type? Handler => Entry?.Handler;
public IReadOnlyDictionary? Parameters => RouteValues;
+
+ private readonly ref struct UriBuffer
+ {
+ private readonly byte[]? _pooled;
+
+ public Span Buffer { get; }
+
+ public UriBuffer(int length)
+ {
+ _pooled = ArrayPool.Shared.Rent(length);
+ Buffer = _pooled.AsSpan(0, length);
+ }
+
+ public UriBuffer(Span buffer) => Buffer = buffer;
+
+ public void Dispose()
+ {
+ if (_pooled != null)
+ {
+ ArrayPool.Shared.Return(_pooled);
+ }
+ }
+ }
}
diff --git a/src/Components/Components/src/Routing/RouteTable.cs b/src/Components/Components/src/Routing/RouteTable.cs
index f657221bdde3..2d4c9335cc87 100644
--- a/src/Components/Components/src/Routing/RouteTable.cs
+++ b/src/Components/Components/src/Routing/RouteTable.cs
@@ -28,6 +28,18 @@ internal static RouteData ProcessParameters(RouteData endpointRouteData)
((Type page, string template) key) => RouteTableFactory.CreateEntry(key.page, key.template));
var routeValueDictionary = new RouteValueDictionary(endpointRouteData.RouteValues);
+ foreach (var kvp in endpointRouteData.RouteValues)
+ {
+ if (kvp.Value is string value)
+ {
+ // At this point the values have already been URL decoded, but we might not have decoded '/' characters.
+ // as that can cause issues when routing the request (You wouldn't be able to accept parameters that contained '/').
+ // To be consistent with existing Blazor quirks that used Uri.UnescapeDataString, we'll replace %2F with /.
+ // We don't want to call Uri.UnescapeDataString here as that would decode other characters that we don't want to decode,
+ // for example, any value that was "double" encoded (for whatever reason) within the original URL.
+ routeValueDictionary[kvp.Key] = value.Replace("%2F", "/", StringComparison.OrdinalIgnoreCase);
+ }
+ }
ProcessParameters(entry, routeValueDictionary);
return new RouteData(endpointRouteData.PageType, routeValueDictionary)
{
@@ -66,6 +78,19 @@ private static void ProcessParameters(InboundRouteEntry entry, RouteValueDiction
}
}
+ foreach (var kvp in routeValues)
+ {
+ if (kvp.Value is string value)
+ {
+ // At this point the values have already been URL decoded, but we might not have decoded '/' characters.
+ // as that can cause issues when routing the request (You wouldn't be able to accept parameters that contained '/').
+ // To be consistent with existing Blazor quirks that used Uri.UnescapeDataString, we'll replace %2F with /.
+ // We don't want to call Uri.UnescapeDataString here as that would decode other characters that we don't want to decode,
+ // for example, any value that was "double" encoded (for whatever reason) within the original URL.
+ routeValues[kvp.Key] = value.Replace("%2F", "/", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
foreach (var parameter in entry.RoutePattern.Parameters)
{
// Add null values for optional route parameters that weren't provided.
diff --git a/src/Components/test/E2ETest/ServerRenderingTests/UnifiedRoutingTests.cs b/src/Components/test/E2ETest/ServerRenderingTests/UnifiedRoutingTests.cs
index 11100ee530a2..d0fce2fb6203 100644
--- a/src/Components/test/E2ETest/ServerRenderingTests/UnifiedRoutingTests.cs
+++ b/src/Components/test/E2ETest/ServerRenderingTests/UnifiedRoutingTests.cs
@@ -24,10 +24,19 @@ public UnifiedRoutingTests(
public override Task InitializeAsync()
=> InitializeAsync(BrowserFixture.StreamingContext);
- [Fact]
- public void Routing_CanRenderPagesWithParameters_And_TransitionToInteractive()
+ [Theory]
+ [InlineData("routing/parameters/value", "value")]
+ // Issue 53138
+ [InlineData("%F0%9F%99%82/routing/parameters/http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback", "http://www.example.com/login/callback")]
+ // Note this double encodes the final 2 slashes
+ [InlineData("%F0%9F%99%82/routing/parameters/http%3A%2F%2Fwww.example.com%2520login%2520callback", "http://www.example.com%20login%20callback")]
+ // Issue 53262
+ [InlineData("routing/parameters/%21%40%23%24%25%5E%26%2A%28%29_%2B-%3D%5B%5D%7B%7D%5C%5C%7C%3B%27%3A%5C%22%3E%3F.%2F", """!@#$%^&*()_+-=[]{}\\|;':\">?./""")]
+ // Issue 52808
+ [InlineData("routing/parameters/parts%20w%2F%20issue", "parts w/ issue")]
+ public void Routing_CanRenderPagesWithParameters_And_TransitionToInteractive(string url, string expectedValue)
{
- ExecuteRoutingTestCore("routing/parameters/value", "value");
+ ExecuteRoutingTestCore(url, expectedValue);
}
[Fact]
@@ -36,10 +45,12 @@ public void Routing_CanRenderPagesWithConstrainedParameters_And_TransitionToInte
ExecuteRoutingTestCore("routing/constraints/5", "5");
}
- [Fact]
- public void Routing_CanRenderPagesWithComplexSegments_And_TransitionToInteractive()
+ [Theory]
+ [InlineData("routing/complex-segment(value)", "value")]
+ [InlineData("%F0%9F%99%82/routing/%F0%9F%99%82complex-segment(http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback)", "http://www.example.com/login/callback")]
+ public void Routing_CanRenderPagesWithComplexSegments_And_TransitionToInteractive(string url, string expectedValue)
{
- ExecuteRoutingTestCore("routing/complex-segment(value)", "value");
+ ExecuteRoutingTestCore(url, expectedValue);
}
[Fact]
@@ -54,10 +65,12 @@ public void Routing_CanRenderPagesWithOptionalParameters_And_TransitionToInterac
ExecuteRoutingTestCore("routing/optional", "null");
}
- [Fact]
- public void Routing_CanRenderPagesWithCatchAllParameters_And_TransitionToInteractive()
+ [Theory]
+ [InlineData("routing/catch-all/rest/of/the/path", "rest/of/the/path")]
+ [InlineData("%F0%9F%99%82/routing/catch-all/http%3A%2F%2Fwww.example.com%2Flogin%2Fcallback/another", "http://www.example.com/login/callback/another")]
+ public void Routing_CanRenderPagesWithCatchAllParameters_And_TransitionToInteractive(string url, string expectedValue)
{
- ExecuteRoutingTestCore("routing/catch-all/rest/of/the/path", "rest/of/the/path");
+ ExecuteRoutingTestCore(url, expectedValue);
}
[Fact]
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedCatchAll.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedCatchAll.razor
new file mode 100644
index 000000000000..94a4010f2a7d
--- /dev/null
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedCatchAll.razor
@@ -0,0 +1,25 @@
+@page "/🙂/routing/catch-all/{*parameter}"
+
+Catch all
+
+@Parameter
+
+@if (_interactive)
+{
+ Rendered interactive.
+}
+
+@code {
+ private bool _interactive;
+
+ [Parameter] public string Parameter { get; set; }
+
+ protected override void OnAfterRender(bool firstRender)
+ {
+ if (firstRender)
+ {
+ _interactive = true;
+ StateHasChanged();
+ }
+ }
+}
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedComplexSegments.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedComplexSegments.razor
new file mode 100644
index 000000000000..aef674fc997b
--- /dev/null
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedComplexSegments.razor
@@ -0,0 +1,24 @@
+@page "/🙂/routing/🙂complex-segment({parameter})"
+Complex segment parameters
+
+@Parameter
+
+@if(_interactive)
+{
+ Rendered interactive.
+}
+
+@code {
+ private bool _interactive;
+
+ [Parameter] public string Parameter { get; set; }
+
+ protected override void OnAfterRender(bool firstRender)
+ {
+ if (firstRender)
+ {
+ _interactive = true;
+ StateHasChanged();
+ }
+ }
+}
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedParameters.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedParameters.razor
new file mode 100644
index 000000000000..0a73e86965e8
--- /dev/null
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/Encoded/EncodedParameters.razor
@@ -0,0 +1,24 @@
+@page "/🙂/routing/parameters/{parameter}"
+Parameters
+
+@Parameter
+
+@if (_interactive)
+{
+ Rendered interactive.
+}
+
+@code {
+ private bool _interactive;
+
+ [Parameter] public string Parameter { get; set; }
+
+ protected override void OnAfterRender(bool firstRender)
+ {
+ if (firstRender)
+ {
+ _interactive = true;
+ StateHasChanged();
+ }
+ }
+}
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/RoutingTestCasesWithEncoding.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/RoutingTestCasesWithEncoding.razor
new file mode 100644
index 000000000000..b91714bdb697
--- /dev/null
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Routing/RoutingTestCasesWithEncoding.razor
@@ -0,0 +1,15 @@
+@page "/🙂/routing"
+@inject NavigationManager NavigationManager
+Routing test cases with encoded urls
+
+