Skip to content

Commit ccb861b

Browse files
authored
Fix verb in route template with gRPC transcoding (#47123)
1 parent 53e77ee commit ccb861b

File tree

7 files changed

+358
-15
lines changed

7 files changed

+358
-15
lines changed

src/Grpc/JsonTranscoding/src/Microsoft.AspNetCore.Grpc.JsonTranscoding/Internal/JsonTranscodingRouteAdapter.cs

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Globalization;
55
using System.Linq;
6+
using System.Text.RegularExpressions;
67
using Grpc.Shared;
78
using Microsoft.AspNetCore.Http;
89

@@ -55,6 +56,8 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
5556
var rewriteActions = new List<Action<HttpContext>>();
5657

5758
var tempSegments = pattern.Segments.ToList();
59+
var haveCatchAll = false;
60+
5861
var i = 0;
5962
while (i < tempSegments.Count)
6063
{
@@ -63,8 +66,16 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
6366
{
6467
var fullPath = string.Join(".", segmentVariable.FieldPath);
6568

66-
var segmentCount = segmentVariable.EndSegment - segmentVariable.StartSegment;
67-
if (segmentCount == 1)
69+
var remainingSegmentCount = segmentVariable.EndSegment - segmentVariable.StartSegment;
70+
71+
// Handle situation where the last segment is catch all but there is a verb.
72+
if (remainingSegmentCount == 1 && segmentVariable.HasCatchAllPath && pattern.Verb != null)
73+
{
74+
// Move past the catch all so the regex added below just includes the verb.
75+
remainingSegmentCount++;
76+
}
77+
78+
if (remainingSegmentCount == 1)
6879
{
6980
// Single segment parameter. Include in route with its default name.
7081
tempSegments[i] = segmentVariable.HasCatchAllPath
@@ -77,7 +88,6 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
7788
var routeParameterParts = new List<string>();
7889
var routeValueFormatTemplateParts = new List<string>();
7990
var variableParts = new List<string>();
80-
var haveCatchAll = false;
8191
var catchAllSuffix = string.Empty;
8292

8393
while (i < segmentVariable.EndSegment && !haveCatchAll)
@@ -101,15 +111,15 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
101111
case SegmentType.CatchAll:
102112
{
103113
var parameterName = $"__Complex_{fullPath}_{i}";
104-
var suffix = string.Join("/", tempSegments.Skip(i + 1));
105-
catchAllSuffix = string.Join("/", tempSegments.Skip(i + segmentCount - 1));
114+
var suffix = BuildSuffix(tempSegments.Skip(i + 1), pattern.Verb);
115+
catchAllSuffix = BuildSuffix(tempSegments.Skip(i + remainingSegmentCount - 1), pattern.Verb);
106116

107117
// It's possible to have multiple routes with catch-all parameters that have different suffixes.
108118
// For example:
109119
// - /{name=v1/**/b}/one
110120
// - /{name=v1/**/b}/two
111121
// The suffix is added as a route constraint to avoid matching multiple routes to a request.
112-
var constraint = suffix.Length > 0 ? $":regex({suffix}$)" : string.Empty;
122+
var constraint = suffix.Length > 0 ? $":regex({Regex.Escape(suffix)}$)" : string.Empty;
113123
tempSegments[i] = $"{{**{parameterName}{constraint}}}";
114124

115125
routeValueFormatTemplateParts.Add($"{{{variableParts.Count}}}");
@@ -145,7 +155,7 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
145155
// the entire remainder of the URL in the route value, we must trim the suffix from that route value.
146156
if (!string.IsNullOrEmpty(catchAllSuffix))
147157
{
148-
finalValue = finalValue.Substring(0, finalValue.Length - catchAllSuffix.Length - 1);
158+
finalValue = finalValue[..^catchAllSuffix.Length];
149159
}
150160
context.Request.RouteValues[fullPath] = finalValue;
151161
});
@@ -169,15 +179,43 @@ public static JsonTranscodingRouteAdapter Parse(HttpRoutePattern pattern)
169179
break;
170180
case SegmentType.CatchAll:
171181
// Ignore remaining segment values.
172-
tempSegments[i] = $"{{**__Discard_{i}}}";
182+
if (pattern.Verb != null)
183+
{
184+
tempSegments[i] = $"{{**__Discard_{i}:regex({Regex.Escape($":{pattern.Verb}")}$)}}";
185+
}
186+
else
187+
{
188+
tempSegments[i] = $"{{**__Discard_{i}}}";
189+
}
190+
haveCatchAll = true;
173191
break;
174192
}
175193

176194
i++;
177195
}
178196
}
179197

180-
return new JsonTranscodingRouteAdapter(pattern, "/" + string.Join("/", tempSegments), rewriteActions);
198+
string resolvedRoutePattern = "/" + string.Join("/", tempSegments);
199+
// If the route has a catch all then the verb is included in the catch all regex constraint.
200+
if (pattern.Verb != null && !haveCatchAll)
201+
{
202+
resolvedRoutePattern += ":" + pattern.Verb;
203+
}
204+
return new JsonTranscodingRouteAdapter(pattern, resolvedRoutePattern, rewriteActions);
205+
206+
static string BuildSuffix(IEnumerable<string> segments, string? verb)
207+
{
208+
var pattern = string.Join("/", segments);
209+
if (!string.IsNullOrEmpty(pattern))
210+
{
211+
pattern = "/" + pattern;
212+
}
213+
if (verb != null)
214+
{
215+
pattern += ":" + verb;
216+
}
217+
return pattern;
218+
}
181219
}
182220

183221
private static SegmentType GetSegmentType(string segment)

src/Grpc/JsonTranscoding/test/Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests/RouteTests.cs

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Net;
45
using System.Net.Http;
56
using System.Net.Http.Headers;
67
using System.Text;
78
using System.Text.Json;
89
using Grpc.Core;
910
using IntegrationTestsWebsite;
1011
using Microsoft.AspNetCore.Grpc.JsonTranscoding.IntegrationTests.Infrastructure;
11-
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal;
12-
using Microsoft.AspNetCore.Grpc.JsonTranscoding.Tests.Infrastructure;
1312
using Microsoft.AspNetCore.Testing;
1413
using Xunit.Abstractions;
1514

@@ -105,4 +104,163 @@ Task<HelloReply> UnaryMethod(ComplextHelloRequest request, ServerCallContext con
105104
// Assert
106105
Assert.Equal("Hello complex_greeter/test2/b last_name!", result.RootElement.GetProperty("message").GetString());
107106
}
107+
108+
[Fact]
109+
public async Task SimpleCatchAllParameter_PrefixSuffixSlashes_MatchUrl_SuccessResult()
110+
{
111+
// Arrange
112+
Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext context)
113+
{
114+
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}!" });
115+
}
116+
var method = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
117+
UnaryMethod,
118+
Greeter.Descriptor.FindMethodByName("SayHelloComplexCatchAll4"));
119+
120+
var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
121+
122+
// Act
123+
var response = await client.GetAsync("/v1/greeter//name/one/two//").DefaultTimeout();
124+
var responseStream = await response.Content.ReadAsStreamAsync();
125+
using var result = await JsonDocument.ParseAsync(responseStream);
126+
127+
// Assert
128+
Assert.Equal("Hello /name/one/two//!", result.RootElement.GetProperty("message").GetString());
129+
}
130+
131+
[Fact]
132+
public async Task ParameterVerb_MatchUrl_SuccessResult()
133+
{
134+
// Arrange
135+
Task<HelloReply> UnaryMethod1(HelloRequest request, ServerCallContext context)
136+
{
137+
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" });
138+
}
139+
Task<HelloReply> UnaryMethod2(HelloRequest request, ServerCallContext context)
140+
{
141+
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" });
142+
}
143+
var method1 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
144+
UnaryMethod1,
145+
Greeter.Descriptor.FindMethodByName("SayHelloCustomVerbOne"));
146+
var method2 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
147+
UnaryMethod2,
148+
Greeter.Descriptor.FindMethodByName("SayHelloCustomVerbTwo"));
149+
150+
var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
151+
152+
// Act 1
153+
var response1 = await client.GetAsync("/v1/greeter_custom/test:one").DefaultTimeout();
154+
var responseStream1 = await response1.Content.ReadAsStreamAsync();
155+
using var result1 = await JsonDocument.ParseAsync(responseStream1);
156+
157+
// Assert 2
158+
Assert.Equal("Hello test one!", result1.RootElement.GetProperty("message").GetString());
159+
160+
// Act 2
161+
var response2 = await client.GetAsync("/v1/greeter_custom/test:two").DefaultTimeout();
162+
var responseStream2 = await response2.Content.ReadAsStreamAsync();
163+
using var result2 = await JsonDocument.ParseAsync(responseStream2);
164+
165+
// Assert 2
166+
Assert.Equal("Hello test two!", result2.RootElement.GetProperty("message").GetString());
167+
168+
// Act 3
169+
var response3 = await client.GetAsync("/v1/greeter_custom/test").DefaultTimeout();
170+
171+
// Assert 3
172+
Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode);
173+
}
174+
175+
[Fact]
176+
public async Task CatchAllVerb_MatchUrl_SuccessResult()
177+
{
178+
// Arrange
179+
Task<HelloReply> UnaryMethod1(HelloRequest request, ServerCallContext context)
180+
{
181+
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" });
182+
}
183+
Task<HelloReply> UnaryMethod2(HelloRequest request, ServerCallContext context)
184+
{
185+
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" });
186+
}
187+
var method1 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
188+
UnaryMethod1,
189+
Greeter.Descriptor.FindMethodByName("SayHelloCatchAllCustomVerbOne"));
190+
var method2 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
191+
UnaryMethod2,
192+
Greeter.Descriptor.FindMethodByName("SayHelloCatchAllCustomVerbTwo"));
193+
194+
var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
195+
196+
// Act 1
197+
var response1 = await client.GetAsync("/v1/greeter_customcatchall/test/name:one").DefaultTimeout();
198+
var responseStream1 = await response1.Content.ReadAsStreamAsync();
199+
using var result1 = await JsonDocument.ParseAsync(responseStream1);
200+
201+
// Assert 2
202+
Assert.Equal("Hello test/name one!", result1.RootElement.GetProperty("message").GetString());
203+
204+
// Act 2
205+
var response2 = await client.GetAsync("/v1/greeter_customcatchall/test/name:two").DefaultTimeout();
206+
var responseStream2 = await response2.Content.ReadAsStreamAsync();
207+
using var result2 = await JsonDocument.ParseAsync(responseStream2);
208+
209+
// Assert 2
210+
Assert.Equal("Hello test/name two!", result2.RootElement.GetProperty("message").GetString());
211+
212+
// Act 3
213+
var response3 = await client.GetAsync("/v1/greeter_customcatchall/test/name").DefaultTimeout();
214+
215+
// Assert 3
216+
Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode);
217+
}
218+
219+
[Fact]
220+
public async Task PostVerb_MatchUrl_SuccessResult()
221+
{
222+
// Arrange
223+
Task<HelloReply> UnaryMethod1(HelloRequest request, ServerCallContext context)
224+
{
225+
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} one!" });
226+
}
227+
Task<HelloReply> UnaryMethod2(HelloRequest request, ServerCallContext context)
228+
{
229+
return Task.FromResult(new HelloReply { Message = $"Hello {request.Name} two!" });
230+
}
231+
var method1 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
232+
UnaryMethod1,
233+
Greeter.Descriptor.FindMethodByName("SayHelloPostCustomVerbOne"));
234+
var method2 = Fixture.DynamicGrpc.AddUnaryMethod<HelloRequest, HelloReply>(
235+
UnaryMethod2,
236+
Greeter.Descriptor.FindMethodByName("SayHelloPostCustomVerbTwo"));
237+
238+
var client = new HttpClient(Fixture.Handler) { BaseAddress = new Uri("http://localhost") };
239+
240+
var requestMessage = new HelloRequest { Name = "test" };
241+
var content = new ByteArrayContent(Encoding.UTF8.GetBytes(requestMessage.ToString()));
242+
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
243+
244+
// Act 1
245+
var response1 = await client.PostAsync("/v1/greeter_custompost:one", content).DefaultTimeout();
246+
var responseStream1 = await response1.Content.ReadAsStreamAsync();
247+
using var result1 = await JsonDocument.ParseAsync(responseStream1);
248+
249+
// Assert 2
250+
Assert.Equal("Hello test one!", result1.RootElement.GetProperty("message").GetString());
251+
252+
// Act 2
253+
var response2 = await client.PostAsync("/v1/greeter_custompost:two", content).DefaultTimeout();
254+
var responseStream2 = await response2.Content.ReadAsStreamAsync();
255+
using var result2 = await JsonDocument.ParseAsync(responseStream2);
256+
257+
// Assert 2
258+
Assert.Equal("Hello test two!", result2.RootElement.GetProperty("message").GetString());
259+
260+
// Act 3
261+
var response3 = await client.PostAsync("/v1/greeter_custompost", content).DefaultTimeout();
262+
263+
// Assert 3
264+
Assert.Equal(HttpStatusCode.NotFound, response3.StatusCode);
265+
}
108266
}

0 commit comments

Comments
 (0)