diff --git a/release_notes.md b/release_notes.md
index 33b74629b8..d4d38a4eb7 100644
--- a/release_notes.md
+++ b/release_notes.md
@@ -16,3 +16,4 @@
- Microsoft.IdentityModel.JsonWebTokens
- Microsoft.IdentityModel.Logging
- Updated Grpc.AspNetCore package to 2.55.0 (https://github.com/Azure/azure-functions-host/pull/9373)
+- Added RestoreRawRequestPath middleware to restore the raw un-decoded request path value to the current request (https://github.com/Azure/azure-functions-host/pull/9402)
diff --git a/src/WebJobs.Script.WebHost/Middleware/RestoreRawRequestPathMiddleware.cs b/src/WebJobs.Script.WebHost/Middleware/RestoreRawRequestPathMiddleware.cs
new file mode 100644
index 0000000000..9c3e255301
--- /dev/null
+++ b/src/WebJobs.Script.WebHost/Middleware/RestoreRawRequestPathMiddleware.cs
@@ -0,0 +1,40 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.Azure.WebJobs.Script.WebHost.Middleware
+{
+ ///
+ /// Middleware to restore the raw request URL path received, in the current request URL.
+ /// App service front end decodes encoded strings in the request URL path.
+ /// This causes routing to fail if the route param value has %2F in it.
+ /// Refer below issues for more context:
+ /// https://github.com/dotnet/aspnetcore/issues/40532#issuecomment-1083562919
+ /// https://github.com/Azure/azure-functions-host/issues/9290.
+ ///
+ internal class RestoreRawRequestPathMiddleware
+ {
+ private readonly RequestDelegate _next;
+ internal const string UnEncodedUrlPathHeaderName = "X-Waws-Unencoded-Url";
+
+ public RestoreRawRequestPathMiddleware(RequestDelegate next)
+ {
+ _next = next ?? throw new ArgumentNullException(nameof(next));
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ if (context.Request.Headers.TryGetValue(UnEncodedUrlPathHeaderName, out var unencodedUrlValue) &&
+ unencodedUrlValue.Count > 0)
+ {
+ context.Request.Path = new PathString(unencodedUrlValue.First());
+ }
+
+ await _next(context);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs
index db7ff7763e..a1cc909bc3 100644
--- a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs
+++ b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs
@@ -35,6 +35,20 @@ public static IApplicationBuilder UseWebJobsScriptHost(this IApplicationBuilder
// Ensure the ClrOptimizationMiddleware is registered before all middleware
builder.UseMiddleware();
+
+ // Update the request URL path (which was altered by App service FE) to raw request path
+ // only if customer has opted in using the app setting.
+ builder.UseWhen(
+ _ => string.Equals(
+ environment.GetEnvironmentVariable(EnvironmentSettingNames.RestoreRawRequestPathEnabled), "1",
+ StringComparison.Ordinal), config =>
+ {
+ config.UseMiddleware();
+ // We need to re-add routing middleware after any updates to request path.
+ config.UseRouting();
+ config.UseEndpoints(endpoints => endpoints.MapControllers());
+ });
+
builder.UseMiddleware();
builder.UseMiddleware();
builder.UseMiddleware();
diff --git a/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs b/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs
index 4d7d416932..d40d0bc636 100644
--- a/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs
+++ b/src/WebJobs.Script/Environment/EnvironmentSettingNames.cs
@@ -138,5 +138,8 @@ public static class EnvironmentSettingNames
public const string AppKind = "APP_KIND";
public const string DrainOnApplicationStopping = "FUNCTIONS_ENABLE_DRAIN_ON_APP_STOPPING";
+
+ // Restore the raw request path from wire to the current request.
+ public const string RestoreRawRequestPathEnabled = "WEBSITE_RESTORE_RAW_REQUEST_PATH";
}
}
diff --git a/test/WebJobs.Script.Tests/Middleware/RestoreRawRequestPathMiddlewareTests.cs b/test/WebJobs.Script.Tests/Middleware/RestoreRawRequestPathMiddlewareTests.cs
new file mode 100644
index 0000000000..c719758d67
--- /dev/null
+++ b/test/WebJobs.Script.Tests/Middleware/RestoreRawRequestPathMiddlewareTests.cs
@@ -0,0 +1,67 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Azure.WebJobs.Script.WebHost.Middleware;
+using Moq;
+using Xunit;
+
+public class RestoreRawRequestPathMiddlewareTests
+{
+ [Fact]
+ public async Task Invoke_WithUnencodedHeaderValue_SetsRequestPath()
+ {
+ // Arrange
+ const string unEncodedHeaderValue = "/cloud%2Fdevdiv";
+ const string requestPathValue = "/cloud/devdiv";
+ var context = CreateHttpContextWithHeader(requestPathValue, unEncodedHeaderValue);
+ var mockNext = new Mock();
+ var middleware = new RestoreRawRequestPathMiddleware(mockNext.Object);
+ var originalPath = context.Request.Path;
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ Assert.NotEqual(new PathString(originalPath), context.Request.Path);
+ Assert.Equal(new PathString(unEncodedHeaderValue), context.Request.Path);
+ mockNext.Verify(next => next(context), Times.Once());
+ }
+
+ [Fact]
+ public async Task Invoke_WithoutUnencodedUrlHeader_DoesNotChangeRequestPath()
+ {
+ // Arrange
+ const string requestPathValue = "/cloud/devdiv";
+ var context = CreateHttpContextWithHeader(requestPathValue);
+ var mockNext = new Mock();
+ var middleware = new RestoreRawRequestPathMiddleware(mockNext.Object);
+ var originalPath = context.Request.Path;
+
+ // Act
+ await middleware.Invoke(context);
+
+ // Assert
+ Assert.Equal(originalPath, context.Request.Path);
+ mockNext.Verify(next => next(context), Times.Once());
+ }
+
+ private static HttpContext CreateHttpContextWithHeader(string requestPathValue, string unEncodedHeaderValue = null)
+ {
+ var context = new DefaultHttpContext
+ {
+ Request =
+ {
+ Path = requestPathValue
+ }
+ };
+
+ if (unEncodedHeaderValue is not null)
+ {
+ context.Request.Headers[RestoreRawRequestPathMiddleware.UnEncodedUrlPathHeaderName] = unEncodedHeaderValue;
+ }
+
+ return context;
+ }
+}