diff --git a/NuGet.config b/NuGet.config
index 1ea5b4e25974..46afdd3217d3 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -6,8 +6,10 @@
+
+
@@ -30,8 +32,10 @@
+
+
diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml
index cd063278122b..56057c740b11 100644
--- a/eng/Version.Details.xml
+++ b/eng/Version.Details.xml
@@ -9,325 +9,325 @@
-->
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-efcore
- 60c2e9b8f50634a077d6c4d2a2a73a7f643c9b11
+ 46c88f8ca38ff2f5b3c202f4d016f23918ab62e8
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
https://github.com/dotnet/xdt
@@ -367,9 +367,9 @@
bc1c3011064a493b0ca527df6fb7215e2e5cfa96
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
@@ -380,9 +380,9 @@
-
+
https://dev.azure.com/dnceng/internal/_git/dotnet-runtime
- a1e6809fb8318884882ceff057000654f558738a
+ f2c8152eed158e72950025393fde498c90a57a6b
https://github.com/dotnet/winforms
diff --git a/eng/Versions.props b/eng/Versions.props
index 7b966dc51531..eb8e6ef0ded3 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -67,92 +67,92 @@
-->
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16-servicing.26229.23
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16-servicing.26229.23
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16-servicing.26229.23
- 9.0.16-servicing.26229.23
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17-servicing.26264.16
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17-servicing.26264.16
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17-servicing.26264.16
+ 9.0.17-servicing.26264.16
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
- 9.0.16-servicing.26229.23
- 9.0.16
+ 9.0.17-servicing.26264.16
+ 9.0.17
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
10.7.0-preview.1.26269.1
10.7.0-preview.1.26269.1
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
- 9.0.16
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
+ 9.0.17
4.11.0-3.24554.2
4.11.0-3.24554.2
@@ -288,7 +288,7 @@
2.64.0
2.64.0
2.64.0
- 2.5.187
+ 2.5.302
3.0.0
3.0.0
3.0.0
diff --git a/global.json b/global.json
index a869acb7952c..d2009ff50396 100644
--- a/global.json
+++ b/global.json
@@ -1,9 +1,9 @@
{
"sdk": {
- "version": "9.0.116"
+ "version": "9.0.118"
},
"tools": {
- "dotnet": "9.0.116",
+ "dotnet": "9.0.118",
"runtimes": {
"dotnet/x86": [
"$(MicrosoftNETCoreBrowserDebugHostTransportVersion)"
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs
index b9c1b70c78c8..7e35ade343bb 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/CallHandlers/CallHandlerDescriptorInfo.cs
@@ -24,7 +24,14 @@ public CallHandlerDescriptorInfo(
BodyFieldDescriptor = bodyFieldDescriptor;
RouteParameterDescriptors = routeParameterDescriptors;
RouteAdapter = routeAdapter;
- PathDescriptorsCache = new ConcurrentDictionary?>();
+ PathDescriptorsCache = new ConcurrentDictionary>();
+
+ var jsonPaths = new HashSet(StringComparer.Ordinal);
+ foreach (var routeParameter in routeParameterDescriptors.Values)
+ {
+ jsonPaths.Add(routeParameter.JsonPath);
+ }
+ RouteParameterJsonPaths = jsonPaths;
}
public FieldDescriptor? ResponseBodyDescriptor { get; }
@@ -34,5 +41,6 @@ public CallHandlerDescriptorInfo(
public FieldDescriptor? BodyFieldDescriptor { get; }
public Dictionary RouteParameterDescriptors { get; }
public JsonTranscodingRouteAdapter RouteAdapter { get; }
- public ConcurrentDictionary?> PathDescriptorsCache { get; }
+ public HashSet RouteParameterJsonPaths { get; }
+ public ConcurrentDictionary> PathDescriptorsCache { get; }
}
diff --git a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs
index 11db47555fbb..1b04bfe84ba8 100644
--- a/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs
+++ b/src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonRequestHelpers.cs
@@ -361,11 +361,18 @@ private static async ValueTask ReadDataAsync(JsonTranscodingServerCallCo
private static List? GetPathDescriptors(JsonTranscodingServerCallContext serverCallContext, IMessage requestMessage, string path)
{
- return serverCallContext.DescriptorInfo.PathDescriptorsCache.GetOrAdd(path, p =>
+ // Must not add null values for paths that don't resolve to a descriptor
+ var cache = serverCallContext.DescriptorInfo.PathDescriptorsCache;
+ if (cache.TryGetValue(path, out var pathDescriptors))
{
- ServiceDescriptorHelpers.TryResolveDescriptors(requestMessage.Descriptor, p.Split('.'), allowJsonName: true, out var pathDescriptors);
return pathDescriptors;
- });
+ }
+ if (ServiceDescriptorHelpers.TryResolveDescriptors(requestMessage.Descriptor, path.Split('.'), allowJsonName: true, out pathDescriptors))
+ {
+ cache.TryAdd(path, pathDescriptors);
+ return pathDescriptors;
+ }
+ return null;
}
public static async ValueTask SendMessage(JsonTranscodingServerCallContext serverCallContext, JsonSerializerOptions serializerOptions, TResponse message, CancellationToken cancellationToken) where TResponse : class
@@ -408,24 +415,25 @@ private static bool CanBindQueryStringVariable(JsonTranscodingServerCallContext
{
if (serverCallContext.DescriptorInfo.BodyDescriptor != null)
{
- var bodyFieldName = serverCallContext.DescriptorInfo.BodyFieldDescriptor?.Name;
+ var bodyFieldDescriptor = serverCallContext.DescriptorInfo.BodyFieldDescriptor;
- // Null field name indicates "*" which means the entire message is bound to the body.
- if (bodyFieldName == null)
+ // Null field descriptor indicates "*" which means the entire message is bound to the body.
+ if (bodyFieldDescriptor?.Name is null)
{
return false;
}
- // Exact match
- if (variable == bodyFieldName)
+ var bodyFieldName = bodyFieldDescriptor.Name;
+ var bodyFieldJsonName = bodyFieldDescriptor.JsonName;
+
+ // Exact match (proto name or JSON name)
+ if (variable == bodyFieldName || variable == bodyFieldJsonName)
{
return false;
}
- // Nested field of field name.
- if (bodyFieldName.Length + 1 < variable.Length &&
- variable.StartsWith(bodyFieldName, StringComparison.Ordinal) &&
- variable[bodyFieldName.Length] == '.')
+ // Nested field of body field (proto name prefix or JSON name prefix).
+ if (IsNestedBodyField(variable, bodyFieldName) || IsNestedBodyField(variable, bodyFieldJsonName))
{
return false;
}
@@ -436,6 +444,20 @@ private static bool CanBindQueryStringVariable(JsonTranscodingServerCallContext
return false;
}
+ // Also check JSON name aliases. Route parameter keys use proto names (e.g. "user_id"),
+ // but query parameters can use JSON names (e.g. "userId") which resolve to the same field.
+ if (serverCallContext.DescriptorInfo.RouteParameterJsonPaths.Contains(variable))
+ {
+ return false;
+ }
+
return true;
}
+
+ private static bool IsNestedBodyField(string variable, string bodyFieldName)
+ {
+ return bodyFieldName.Length + 1 < variable.Length &&
+ variable.StartsWith(bodyFieldName, StringComparison.Ordinal) &&
+ variable[bodyFieldName.Length] == '.';
+ }
}
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs
index 5beffece90fa..b1f4bb70ac52 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs
@@ -105,6 +105,37 @@ Task UnaryMethod(ComplextHelloRequest request, ServerCallContext con
Assert.Equal("Hello complex_greeter/test2/b last_name!", result.RootElement.GetProperty("message").GetString());
}
+ [Fact]
+ public async Task ComplexParameter_NestedJsonNameQueryString_DoesNotOverwriteRouteValue()
+ {
+ // Arrange
+ Task UnaryMethod(ComplextHelloRequest request, ServerCallContext context)
+ {
+ return Task.FromResult(new HelloReply { Message = $"Hello {request.Name.FirstName} {request.Name.LastName}!" });
+ }
+ var method = Fixture.DynamicGrpc.AddUnaryMethod(
+ UnaryMethod,
+ Greeter.Descriptor.FindMethodByName("SayHelloComplexCatchAll3"));
+
+ var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
+
+ // route.binding sets
+ // request.Name.LastName = "last_name";
+ // request.Name.FirstName = "complex_greeter/test2/b"
+ // ----
+ // query binding tries to overwrite with
+ // name.firstName=query_first
+ // name.lastName=query_last
+
+ // Act
+ var response = await client.GetAsync("/v1/last_name/complex_greeter/test2/b/c/d/two?name.firstName=query_first&name.lastName=query_last").DefaultTimeout();
+ var responseStream = await response.Content.ReadAsStreamAsync();
+ using var result = await JsonDocument.ParseAsync(responseStream);
+
+ // Assert
+ Assert.Equal("Hello complex_greeter/test2/b last_name!", result.RootElement.GetProperty("message").GetString());
+ }
+
[Fact]
public async Task SimpleCatchAllParameter_PrefixSuffixSlashes_MatchUrl_SuccessResult()
{
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto
index e44535586bba..7b817554e737 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/Proto/transcoding.proto
@@ -218,6 +218,7 @@ message HelloRequest {
string hiding_field_name = 24 [json_name="field_name"];
repeated SubMessage repeated_messages = 25;
map map_keyint_valueint = 26;
+ SubMessage sub_data = 27;
}
message HelloReply {
diff --git a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs
index 1c5b03d79483..5243effc4d63 100644
--- a/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs
+++ b/src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests/UnaryServerCallHandlerTests.cs
@@ -40,6 +40,12 @@ private static RouteParameter CreateRouteParameter(List descrip
return new RouteParameter(descriptorPath, new HttpRouteVariable(), string.Empty);
}
+ private static RouteParameter CreateRouteParameterWithJsonPath(List descriptorPath)
+ {
+ var jsonPath = string.Join(".", descriptorPath.Select(d => d.JsonName));
+ return new RouteParameter(descriptorPath, new HttpRouteVariable(), jsonPath);
+ }
+
[Fact]
public async Task HandleCallAsync_MatchingRouteValue_SetOnRequestMessage()
{
@@ -1504,6 +1510,43 @@ public async Task HandleCallAsync_MatchingRepeatedQueryStringValues_SetOnRequest
Assert.Equal("TestSubfields2!", request!.Sub.Subfields[1]);
}
+ [Fact]
+ public async Task HandleCallAsync_UnmatchedQueryStringValues_NotCached()
+ {
+ // Arrange
+ HelloRequest? request = null;
+ UnaryServerMethod invoker = (s, r, c) =>
+ {
+ request = r;
+ return Task.FromResult(new HelloReply());
+ };
+ var descriptorInfo = TestHelpers.CreateDescriptorInfo();
+ var unaryServerCallHandler = CreateCallHandler(invoker, descriptorInfo);
+ var httpContext = TestHelpers.CreateHttpContext();
+ httpContext.Request.Query = new QueryCollection(new Dictionary
+ {
+ ["age"] = "10",
+ ["sub.subfield"] = "TestSubfield!",
+ ["unknown"] = "value",
+ ["sub.unknown"] = "value",
+ ["name.unknown"] = "value"
+ });
+ // Act
+ await unaryServerCallHandler.HandleCallAsync(httpContext);
+ // Assert
+ Assert.NotNull(request);
+ Assert.Equal(10, request!.Age);
+ Assert.Equal("TestSubfield!", request!.Sub.Subfield);
+ // matches query params
+ Assert.Equal(2, descriptorInfo.PathDescriptorsCache.Count);
+ Assert.True(descriptorInfo.PathDescriptorsCache.ContainsKey("age"));
+ Assert.True(descriptorInfo.PathDescriptorsCache.ContainsKey("sub.subfield"));
+ // not matched query params
+ Assert.False(descriptorInfo.PathDescriptorsCache.ContainsKey("unknown"));
+ Assert.False(descriptorInfo.PathDescriptorsCache.ContainsKey("sub.unknown"));
+ Assert.False(descriptorInfo.PathDescriptorsCache.ContainsKey("name.unknown"));
+ }
+
[Fact]
public async Task HandleCallAsync_DataTypes_SetOnRequestMessage()
{
@@ -1803,6 +1846,186 @@ public async Task HandleCallAsync_MatchingQueryStringValues_KnownType_FieldSette
Assert.Equal(fieldmask, request!.FieldMaskValue);
}
+ [Fact]
+ public async Task HandleCallAsync_QueryStringJsonNameAlias_DoesNotOverwriteRouteValue()
+ {
+ // message HelloRequest {
+ // string name = 1; // proto name: "name", JSON name: "name"
+ // int32 age = 13; // proto name: "age", JSON name: "age"
+ // string field_name = 22 [json_name="json_customized_name"]; // proto name: "field_name", JSON name: "json_customized_name"
+ // ... other fields
+ // }
+
+ // Arrange
+ // A query parameter using the JSON name should not overwrite a route-bound field.
+ HelloRequest? request = null;
+ UnaryServerMethod invoker = (s, r, c) =>
+ {
+ request = r;
+ return Task.FromResult(new HelloReply());
+ };
+
+ var fieldDescriptor = HelloRequest.Descriptor.FindFieldByName("field_name");
+ var routeParameterDescriptors = new Dictionary
+ {
+ ["field_name"] = CreateRouteParameterWithJsonPath(new List(new[] { fieldDescriptor }))
+ };
+ var descriptorInfo = TestHelpers.CreateDescriptorInfo(routeParameterDescriptors: routeParameterDescriptors);
+ var unaryServerCallHandler = CreateCallHandler(invoker, descriptorInfo: descriptorInfo);
+ var httpContext = TestHelpers.CreateHttpContext();
+ httpContext.Request.RouteValues["field_name"] = "route_value";
+ httpContext.Request.Query = new QueryCollection(new Dictionary
+ {
+ ["json_customized_name"] = "different_value"
+ });
+
+ // Act
+ await unaryServerCallHandler.HandleCallAsync(httpContext);
+
+ // Assert
+ Assert.NotNull(request);
+ Assert.Equal("route_value", request!.FieldName);
+ }
+
+ [Fact]
+ public async Task HandleCallAsync_QueryStringProtoName_DoesNotOverwriteRouteValue()
+ {
+ // Arrange
+ // A query parameter using the proto name is not overwritting the route-bound field.
+ HelloRequest? request = null;
+ UnaryServerMethod invoker = (s, r, c) =>
+ {
+ request = r;
+ return Task.FromResult(new HelloReply());
+ };
+
+ var routeParameterDescriptors = new Dictionary
+ {
+ ["name"] = CreateRouteParameterWithJsonPath(new List(new[] { HelloRequest.Descriptor.FindFieldByNumber(HelloRequest.NameFieldNumber) }))
+ };
+ var descriptorInfo = TestHelpers.CreateDescriptorInfo(routeParameterDescriptors: routeParameterDescriptors);
+ var unaryServerCallHandler = CreateCallHandler(invoker, descriptorInfo: descriptorInfo);
+ var httpContext = TestHelpers.CreateHttpContext();
+ httpContext.Request.RouteValues["name"] = "route_value";
+ httpContext.Request.Query = new QueryCollection(new Dictionary
+ {
+ ["name"] = "different_value"
+ });
+
+ // Act
+ await unaryServerCallHandler.HandleCallAsync(httpContext);
+
+ // Assert
+ Assert.NotNull(request);
+ Assert.Equal("route_value", request!.Name);
+ }
+
+ [Fact]
+ public async Task HandleCallAsync_QueryStringNonRouteField_StillBindsNormally()
+ {
+ // Arrange
+ // Query parameters for fields NOT bound via route should work.
+ HelloRequest? request = null;
+ UnaryServerMethod invoker = (s, r, c) =>
+ {
+ request = r;
+ return Task.FromResult(new HelloReply());
+ };
+
+ var routeParameterDescriptors = new Dictionary
+ {
+ ["name"] = CreateRouteParameterWithJsonPath(new List(new[] { HelloRequest.Descriptor.FindFieldByNumber(HelloRequest.NameFieldNumber) }))
+ };
+ var descriptorInfo = TestHelpers.CreateDescriptorInfo(routeParameterDescriptors: routeParameterDescriptors);
+ var unaryServerCallHandler = CreateCallHandler(invoker, descriptorInfo: descriptorInfo);
+ var httpContext = TestHelpers.CreateHttpContext();
+ httpContext.Request.RouteValues["name"] = "route_value";
+ httpContext.Request.Query = new QueryCollection(new Dictionary
+ {
+ ["age"] = "30"
+ });
+
+ // Act
+ await unaryServerCallHandler.HandleCallAsync(httpContext);
+
+ // Assert
+ Assert.NotNull(request);
+ Assert.Equal("route_value", request!.Name);
+ Assert.Equal(30, request!.Age);
+ }
+
+ [Fact]
+ public async Task HandleCallAsync_QueryStringBodyFieldJsonNameAlias_DoesNotOverwriteBodyValue()
+ {
+ // Arrange
+ // body: "sub" binds the sub field from JSON body.
+ // A query parameter using the JSON name of the body field should not overwrite it.
+ HelloRequest? request = null;
+ UnaryServerMethod invoker = (s, r, c) =>
+ {
+ request = r;
+ return Task.FromResult(new HelloReply());
+ };
+
+ var descriptorInfo = TestHelpers.CreateDescriptorInfo(
+ bodyDescriptor: HelloRequest.Types.SubMessage.Descriptor,
+ bodyFieldDescriptor: HelloRequest.Descriptor.FindFieldByName("sub"));
+ var unaryServerCallHandler = CreateCallHandler(invoker, descriptorInfo: descriptorInfo);
+ var httpContext = TestHelpers.CreateHttpContext();
+ httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonFormatter.Default.Format(new HelloRequest.Types.SubMessage
+ {
+ Subfield = "body_value"
+ })));
+ httpContext.Request.ContentType = "application/json";
+ httpContext.Request.Query = new QueryCollection(new Dictionary
+ {
+ ["sub.subfield"] = "different_value"
+ });
+
+ // Act
+ await unaryServerCallHandler.HandleCallAsync(httpContext);
+
+ // Assert
+ Assert.NotNull(request);
+ Assert.Equal("body_value", request!.Sub.Subfield);
+ }
+
+ [Fact]
+ public async Task HandleCallAsync_QueryStringBodyFieldJsonNamePrefix_DoesNotOverwriteBodyValue()
+ {
+ // Arrange
+ // body field: "sub_data" (proto name) has JSON name "subData".
+ // A query parameter using the JSON name prefix "subData.subfield" should be blocked.
+ HelloRequest? request = null;
+ UnaryServerMethod invoker = (s, r, c) =>
+ {
+ request = r;
+ return Task.FromResult(new HelloReply());
+ };
+
+ var descriptorInfo = TestHelpers.CreateDescriptorInfo(
+ bodyDescriptor: HelloRequest.Types.SubMessage.Descriptor,
+ bodyFieldDescriptor: HelloRequest.Descriptor.FindFieldByName("sub_data"));
+ var unaryServerCallHandler = CreateCallHandler(invoker, descriptorInfo: descriptorInfo);
+ var httpContext = TestHelpers.CreateHttpContext();
+ httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonFormatter.Default.Format(new HelloRequest.Types.SubMessage
+ {
+ Subfield = "body_value"
+ })));
+ httpContext.Request.ContentType = "application/json";
+ httpContext.Request.Query = new QueryCollection(new Dictionary
+ {
+ ["subData.subfield"] = "different_value"
+ });
+
+ // Act
+ await unaryServerCallHandler.HandleCallAsync(httpContext);
+
+ // Assert
+ Assert.NotNull(request);
+ Assert.Equal("body_value", request!.SubData.Subfield);
+ }
+
private UnaryServerCallHandler CreateCallHandler(
UnaryServerMethod invoker,
CallHandlerDescriptorInfo? descriptorInfo = null,
diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs
index cc9ea67ec2dd..1f633dbaff83 100644
--- a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs
+++ b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs
@@ -50,6 +50,12 @@ ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, Canc
return ValueTask.CompletedTask;
}
+ if (context.HttpContext.User?.Identity?.IsAuthenticated == true)
+ {
+ context.AllowCacheStorage = false;
+ return ValueTask.CompletedTask;
+ }
+
// Check response code
if (response.StatusCode != StatusCodes.Status200OK)
{
diff --git a/src/Middleware/OutputCaching/test/OutputCacheTests.cs b/src/Middleware/OutputCaching/test/OutputCacheTests.cs
index 85de08aadc6d..dc50e960fcf7 100644
--- a/src/Middleware/OutputCaching/test/OutputCacheTests.cs
+++ b/src/Middleware/OutputCaching/test/OutputCacheTests.cs
@@ -1,10 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Net;
using System.Net.Http;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.OutputCaching.Tests;
@@ -1030,6 +1034,190 @@ static async Task RunClient(TestServer server, int id)
}
}
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task AuthenticatedRequestsAreNotCached(bool authMiddlewareBeforeCache)
+ {
+ int finalEndpointHitCount = 0;
+ var builder = new HostBuilder()
+ .ConfigureWebHost(webHostBuilder =>
+ {
+ webHostBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddOutputCache(outputCachingOptions =>
+ {
+ outputCachingOptions.BasePolicies = [new OutputCachePolicyBuilder().Build()];
+ });
+ })
+ .Configure(app =>
+ {
+ if (authMiddlewareBeforeCache)
+ {
+ AddAuth(app);
+ }
+
+ app.UseOutputCache();
+
+ if (!authMiddlewareBeforeCache)
+ {
+ AddAuth(app);
+ }
+
+ app.Run(async context =>
+ {
+ finalEndpointHitCount++;
+ if (context.User.Identity?.IsAuthenticated == true)
+ {
+ await context.Response.WriteAsync(context.User.Identity.Name ?? "anonymous (authenticated)");
+ }
+ else
+ {
+ await context.Response.WriteAsync(context.User.Identity?.Name ?? "anonymous");
+ }
+ });
+ });
+ });
+
+ using var host = builder.Build();
+
+ await host.StartAsync();
+
+ using var server = host.GetTestServer();
+
+ var iterations = 10;
+ for (int i = 0; i < iterations; i++)
+ {
+ await RunClient(server, i);
+ }
+
+ // RunClient sends two requests per iteration
+ Assert.Equal(iterations * 2, finalEndpointHitCount);
+
+ // Unauthenticated request, check it still works
+ var client = server.CreateClient();
+ var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, ""));
+ Assert.Equal("anonymous", await resp.Content.ReadAsStringAsync());
+
+ var resp2 = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, ""));
+ Assert.Equal("anonymous", await resp2.Content.ReadAsStringAsync());
+ // Smoke test that unauthenticated request can be served from cache.
+ Assert.True(resp2.Headers.Contains(HeaderNames.Age));
+
+ static async Task RunClient(TestServer server, int i)
+ {
+ var client = server.CreateClient();
+ // Use headers for name since default vary-by uses query and we want the requests to look the same to the caching middleware
+ var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "") { Headers = { { "name", $"{i}" } } });
+ Assert.Equal($"{i}", await resp.Content.ReadAsStringAsync());
+
+ var resp2 = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "") { Headers = { { "name", $"{i}" } } });
+ Assert.Equal($"{i}", await resp2.Content.ReadAsStringAsync());
+ // Smoke test that authenticated request isn't served from cache.
+ Assert.False(resp2.Headers.Contains(HeaderNames.Age));
+ }
+
+ static void AddAuth(IApplicationBuilder app)
+ {
+ app.Use((c, n) =>
+ {
+ if (c.Request.Headers.ContainsKey("name"))
+ {
+ c.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, c.Request.Headers["name"]) }, authenticationType: "custom"));
+ Assert.True(c.User.Identity?.IsAuthenticated);
+ }
+
+ return n(c);
+ });
+ }
+ }
+
+ // This is a negative test to show that if auth middleware is after caching, the auth middleware won't run if a cache entry exists
+ // which is why we recommend putting auth before caching so that you don't accidentally serve anonymous cached content to an authenticated user.
+ [Fact]
+ public async Task AuthMiddlewareAfterCachingNotRunIfCacheEntryExists()
+ {
+ int finalEndpointHitCount = 0;
+ var builder = new HostBuilder()
+ .ConfigureWebHost(webHostBuilder =>
+ {
+ webHostBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddOutputCache(outputCachingOptions =>
+ {
+ outputCachingOptions.BasePolicies = [new OutputCachePolicyBuilder().Build()];
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseOutputCache();
+ // Auth middleware
+ app.Use((c, n) =>
+ {
+ if (c.Request.Headers.ContainsKey("name"))
+ {
+ c.User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, c.Request.Headers["name"]) }, authenticationType: "custom"));
+ }
+
+ return n(c);
+ });
+ app.Run(async context =>
+ {
+ finalEndpointHitCount++;
+ if (context.User.Identity?.IsAuthenticated == true)
+ {
+ await context.Response.WriteAsync(context.User.Identity.Name ?? "anonymous (authenticated)");
+ }
+ else
+ {
+ await context.Response.WriteAsync(context.User.Identity?.Name ?? "anonymous");
+ }
+ });
+ });
+ });
+
+ using var host = builder.Build();
+
+ await host.StartAsync();
+
+ using var server = host.GetTestServer();
+
+ // Make an unauthenticated request first to add a cache entry.
+ var client = server.CreateClient();
+ var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, ""));
+ Assert.Equal("anonymous", await resp.Content.ReadAsStringAsync());
+
+ var resp2 = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, ""));
+ Assert.Equal("anonymous", await resp2.Content.ReadAsStringAsync());
+ // Smoke test that unauthenticated request is served from cache.
+ Assert.True(resp2.Headers.Contains(HeaderNames.Age));
+
+ var iterations = 10;
+ for (int i = 0; i < iterations; i++)
+ {
+ await RunClient(server, i);
+ }
+
+ // Endpoint only hit once, for first request that doesn't have auth.
+ Assert.Equal(1, finalEndpointHitCount);
+
+ static async Task RunClient(TestServer server, int i)
+ {
+ var client = server.CreateClient();
+ // Use headers for name since default vary-by uses query and we want the requests to look the same to the caching middleware
+ var resp = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "") { Headers = { { "name", $"{i}" } } });
+
+ // Because the output cache middleware is before auth, these requests will serve a cached response if it exists (and we made one exist for this test)
+ // While this isn't ideal, we recommend app developers put caching after the auth middleware where we will skip the cache layer.
+ Assert.Equal("anonymous", await resp.Content.ReadAsStringAsync());
+ Assert.True(resp.Headers.Contains(HeaderNames.Age));
+ }
+ }
+
private static void Assert304Headers(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse)
{
// https://tools.ietf.org/html/rfc7232#section-4.1
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
index 42237f871d74..0bd2fcd06ab5 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
@@ -442,8 +442,11 @@ private bool TryValidatePath(ReadOnlySpan pathSegment)
for (var i = 0; i < pathSegment.Length; i++)
{
var ch = pathSegment[i];
- // The header parser should already be checking this
- Debug.Assert(32 < ch && ch < 127);
+ if (ch > byte.MaxValue)
+ {
+ ResetAndAbort(new ConnectionAbortedException(CoreStrings.FormatHttp2StreamErrorPathInvalid(RawTarget)), Http2ErrorCode.PROTOCOL_ERROR);
+ return false;
+ }
pathBuffer[i] = (byte)ch;
}
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
index 832ba5b7540a..fe8ee4d755df 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
@@ -1183,8 +1183,11 @@ private bool TryValidatePath(ReadOnlySpan pathSegment)
for (var i = 0; i < pathSegment.Length; i++)
{
var ch = pathSegment[i];
- // The header parser should already be checking this
- Debug.Assert(32 < ch && ch < 127);
+ if (ch > byte.MaxValue)
+ {
+ Abort(new ConnectionAbortedException(CoreStrings.FormatHttp3StreamErrorPathInvalid(RawTarget)), Http3ErrorCode.ProtocolError);
+ return false;
+ }
pathBuffer[i] = (byte)ch;
}
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index fe3190b2f26a..9b71c7c080e1 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -791,6 +791,31 @@ public async Task HEADERS_Received_MaxRequestLineSize_Reset()
await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
}
+ [Theory]
+ [InlineData("/\u0161")]
+ [InlineData("/a\u0161")]
+ [InlineData("/\u0161a")]
+ [InlineData("/a\u0161a")]
+ public async Task HEADERS_Received_CharacterLargerThanByte_Reset(string path)
+ {
+ var pathBytes = Encoding.UTF8.GetBytes(path);
+ var headerBlock = new byte[4 + pathBytes.Length];
+ headerBlock[0] = 0x82; // Indexed: :method GET (HPACK static index 2)
+ headerBlock[1] = 0x86; // Indexed: :scheme http (HPACK static index 6)
+ headerBlock[2] = 0x44; // Literal incremental indexing, name index 4 (:path)
+ headerBlock[3] = (byte)pathBytes.Length; // Value length, no Huffman
+ pathBytes.CopyTo(headerBlock.AsSpan(4));
+
+ await InitializeConnectionAsync(_noopApplication);
+
+ await StartStreamAsync(1, headerBlock, endStream: true);
+
+ await WaitForStreamErrorAsync(expectedStreamId: 1, Http2ErrorCode.PROTOCOL_ERROR,
+ CoreStrings.FormatHttp2StreamErrorPathInvalid(path));
+
+ await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+ }
+
[Fact]
public async Task HEADERS_Received_MaxRequestHeadersTotalSize_431()
{
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
index b7697117452e..cd91c2bd7eb9 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
@@ -255,6 +255,36 @@ public async Task Path_DecodedAndNormalized(string input, string expected)
Assert.Equal("0", responseHeaders["content-length"]);
}
+ [Theory]
+ [InlineData("/\u0161dmin?x=1")] // U+0161 (353), truncates to 0x61 = 'a' → "/admin?x=1"
+ [InlineData("/\u0170ser")] // U+0170 (368), truncates to 0x70 = 'p' → "/pser"
+ [InlineData("/caf\u0165?q=1")] // U+0165 (357), truncates to 0x65 = 'e' → "/cafe?q=1
+ public async Task NonAsciiPath_Reset(string path)
+ {
+ var pathBytes = Encoding.UTF8.GetBytes(path);
+ var qpackBlock = new byte[6 + pathBytes.Length];
+ qpackBlock[0] = 0x00; // Required Insert Count = 0
+ qpackBlock[1] = 0x00; // Delta Base = 0
+ qpackBlock[2] = 0xD1; // Indexed: :method GET (QPACK static index 17)
+ qpackBlock[3] = 0xD6; // Indexed: :scheme http (QPACK static index 22)
+ qpackBlock[4] = 0x51; // Literal with static name ref, index 1 (:path)
+ qpackBlock[5] = (byte)pathBytes.Length; // Value length, no Huffman
+ pathBytes.CopyTo(qpackBlock.AsSpan(6));
+
+ await Http3Api.InitializeConnectionAsync(_noopApplication);
+ Http3Api.OutboundControlStream = await Http3Api.CreateControlStream();
+
+ var requestStream = await Http3Api.CreateRequestStream(
+ headers: (IEnumerable>)null, endStream: false);
+
+ await requestStream.SendFrameAsync(Http3FrameType.Headers, qpackBlock, endStream: true);
+
+ await requestStream.WaitForStreamErrorAsync(
+ Http3ErrorCode.ProtocolError,
+ AssertExpectedErrorMessages,
+ CoreStrings.FormatHttp3StreamErrorPathInvalid(path));
+ }
+
[Theory]
[InlineData(":path", "/")]
[InlineData(":scheme", "http")]
diff --git a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTestBase.cs b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTestBase.cs
index 83c1f522b0b0..b748980724eb 100644
--- a/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTestBase.cs
+++ b/src/SignalR/common/SignalR.Common/test/Internal/Protocol/MessagePackHubProtocolTestBase.cs
@@ -258,6 +258,29 @@ public void ParseMessageWithExtraData()
Assert.Equal(expectedMessage, message, TestHubMessageEqualityComparer.Instance);
}
+ // Unknown invocation IDs are skipped over. We still need to parse the message
+ // to get to the next message, but we won't create any objects (except the outer hubmessage type)
+ [Fact]
+ public void SkipResult_WithDeeplyNestedStructure()
+ {
+ var expectedMessage = CompletionMessage.WithResult("xyz", null);
+ // Verify that the input binary string decodes to the expected MsgPack primitives
+ var bytes = new byte[] { ArrayBytes(5),
+ 3, // Completion message
+ 0x80, // Empty headers
+ StringBytes(3), (byte)'x', (byte)'y', (byte)'z', // invocation ID
+ 3, // result type; non-void
+ }
+ .Concat(Enumerable.Repeat((byte)0x91, 100_000).Append((byte)0xC0)) // 100,000 nested arrays with a null at the end
+ .ToArray();
+
+ bytes = Frame(bytes);
+
+ var data = new ReadOnlySequence(bytes);
+ // Null types for TestBinder so parser will skip over the result (treats it as an unknown invocation ID)
+ Assert.True(HubProtocol.TryParseMessage(ref data, new TestBinder(), out var message));
+ }
+
[Theory]
[MemberData(nameof(BaseTestDataNames))]
public void BaseWriteMessages(string testDataName)
diff --git a/src/submodules/MessagePack-CSharp b/src/submodules/MessagePack-CSharp
index 9aeb12b9bdb0..9614e6f396e9 160000
--- a/src/submodules/MessagePack-CSharp
+++ b/src/submodules/MessagePack-CSharp
@@ -1 +1 @@
-Subproject commit 9aeb12b9bdb024512ffe2e4bddfa2785dca6e39e
+Subproject commit 9614e6f396e959ad67e4ba655d5ab6e1311bed23