Skip to content

Commit af93d32

Browse files
authored
Add option to ignore calls to unknown gRPC services (#788)
1 parent 1159126 commit af93d32

File tree

8 files changed

+105
-14
lines changed

8 files changed

+105
-14
lines changed

perf/benchmarkapps/GrpcAspNetCoreServer/Startup.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ public class Startup
3737
{
3838
public void ConfigureServices(IServiceCollection services)
3939
{
40-
services.AddGrpc();
40+
services.AddGrpc(o =>
41+
{
42+
// Small performance benefits to not add catch-all routes to handle UNIMPLEMENTED for unknown services
43+
o.IgnoreUnknownServices = true;
44+
});
4145
services.AddControllers();
4246

4347
#if CLIENT_CERTIFICATE_AUTHENTICATION

src/Grpc.AspNetCore.Server/GrpcServiceOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ public IList<ICompressionProvider> CompressionProviders
7474
set => _compressionProviders = value;
7575
}
7676

77+
/// <summary>
78+
/// Gets or sets a value indicating whether gRPC should ignore calls to unknown services and methods.
79+
/// If set to <c>true</c>, calls to unknown services and methods won't return an 'UNIMPLEMENTED' status,
80+
/// and the request will pass to the next registered middleware in ASP.NET Core.
81+
/// </summary>
82+
public bool? IgnoreUnknownServices { get; set; }
83+
7784
/// <summary>
7885
/// Get a collection of interceptors to be executed with every call. Interceptors are executed in order.
7986
/// </summary>

src/Grpc.AspNetCore.Server/Internal/GrpcServiceOptionsSetup.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public void Configure(GrpcServiceOptions<TService> options)
6060
options.ResponseCompressionAlgorithm = _options.ResponseCompressionAlgorithm;
6161
options.ResponseCompressionLevel = _options.ResponseCompressionLevel;
6262
options.CompressionProviders = _options.CompressionProviders;
63+
options.IgnoreUnknownServices = _options.IgnoreUnknownServices;
6364
}
6465
}
6566
}

src/Grpc.AspNetCore.Server/Internal/ServerCallHandlerFactory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ public RequestDelegate CreateUnimplementedMethod()
112112
};
113113
}
114114

115+
public bool IgnoreUnknownServices => _globalOptions.IgnoreUnknownServices ?? false;
116+
public bool IgnoreUnknownMethods => _serviceOptions.IgnoreUnknownServices ?? false;
117+
115118
public RequestDelegate CreateUnimplementedService()
116119
{
117120
var logger = _loggerFactory.CreateLogger<ServerCallHandlerFactory<TService>>();

src/Grpc.AspNetCore.Server/Model/Internal/ServiceRouteBuilder.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -106,27 +106,30 @@ internal static void CreateUnimplementedEndpoints(
106106
{
107107
// Return UNIMPLEMENTED status for missing service:
108108
// - /{service}/{method} + content-type header = grpc/application
109-
if (serviceMethodsRegistry.Methods.Count == 0)
109+
if (!serverCallHandlerFactory.IgnoreUnknownServices && serviceMethodsRegistry.Methods.Count == 0)
110110
{
111111
// Only one unimplemented service endpoint is needed for the application
112112
CreateUnimplementedEndpoint(endpointRouteBuilder, "{unimplementedService}/{unimplementedMethod}", "Unimplemented service", serverCallHandlerFactory.CreateUnimplementedService());
113113
}
114114

115115
// Return UNIMPLEMENTED status for missing method:
116116
// - /Package.Service/{method} + content-type header = grpc/application
117-
var serviceNames = serviceMethods.Select(m => m.Method.ServiceName).Distinct();
118-
119-
// Typically there should be one service name for a type
120-
// In case the bind method sets up multiple services in one call we'll loop over them
121-
foreach (var serviceName in serviceNames)
117+
if (!serverCallHandlerFactory.IgnoreUnknownMethods)
122118
{
123-
if (serviceMethodsRegistry.Methods.Any(m => string.Equals(m.Method.ServiceName, serviceName, StringComparison.Ordinal)))
119+
var serviceNames = serviceMethods.Select(m => m.Method.ServiceName).Distinct();
120+
121+
// Typically there should be one service name for a type
122+
// In case the bind method sets up multiple services in one call we'll loop over them
123+
foreach (var serviceName in serviceNames)
124124
{
125-
// Only one unimplemented method endpoint is need for the service
126-
continue;
127-
}
125+
if (serviceMethodsRegistry.Methods.Any(m => string.Equals(m.Method.ServiceName, serviceName, StringComparison.Ordinal)))
126+
{
127+
// Only one unimplemented method endpoint is need for the service
128+
continue;
129+
}
128130

129-
CreateUnimplementedEndpoint(endpointRouteBuilder, serviceName + "/{unimplementedMethod}", $"Unimplemented method for {serviceName}", serverCallHandlerFactory.CreateUnimplementedMethod());
131+
CreateUnimplementedEndpoint(endpointRouteBuilder, serviceName + "/{unimplementedMethod}", $"Unimplemented method for {serviceName}", serverCallHandlerFactory.CreateUnimplementedMethod());
132+
}
130133
}
131134
}
132135

test/FunctionalTests/Web/Client/ServerStreamingMethodTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ public async Task SendValidRequest_ClientAbort_ClientThrowsCancelledException()
154154

155155
Assert.AreEqual(StatusCode.Cancelled, call.GetStatus().StatusCode);
156156

157+
// It is possible get into a situation where the response stream finishes slightly before the call.
158+
// Small delay to ensure call logging is complete.
159+
await Task.Delay(50);
160+
157161
AssertHasLog(LogLevel.Information, "GrpcStatusError", "Call failed with gRPC error status. Status code: 'Cancelled', Message: 'Call canceled by the client.'.");
158162
}
159163
}

test/Grpc.AspNetCore.Server.Tests/GrpcEndpointRouteBuilderExtensionsTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,69 @@ public void MapGrpcService_NoMatchingCompressionProvider_ThrowError()
296296
Assert.AreEqual("The configured response compression algorithm 'DOES_NOT_EXIST' does not have a matching compression provider.", ex.InnerException!.InnerException!.Message);
297297
}
298298

299+
[Test]
300+
public void MapGrpcService_IgnoreUnknownServicesDefault_RegisterUnknownHandler()
301+
{
302+
// Arrange
303+
var services = ServicesHelpers.CreateServices();
304+
305+
var routeBuilder = CreateTestEndpointRouteBuilder(services.BuildServiceProvider(validateScopes: true));
306+
307+
// Act
308+
routeBuilder.MapGrpcService<GreeterServiceWithMetadataAttributes>();
309+
310+
// Assert
311+
var endpoints = routeBuilder.DataSources
312+
.SelectMany(ds => ds.Endpoints)
313+
.ToList();
314+
315+
Assert.IsNotNull(endpoints.SingleOrDefault(e => e.DisplayName == "gRPC - Unimplemented service"));
316+
Assert.IsNotNull(endpoints.SingleOrDefault(e => e.DisplayName == "gRPC - Unimplemented method for greet.Greeter"));
317+
}
318+
319+
[Test]
320+
public void MapGrpcService_IgnoreUnknownServicesGlobalTrue_DontRegisterUnknownHandler()
321+
{
322+
// Arrange
323+
var services = ServicesHelpers.CreateServices(o => o.IgnoreUnknownServices = true);
324+
325+
var routeBuilder = CreateTestEndpointRouteBuilder(services.BuildServiceProvider(validateScopes: true));
326+
327+
// Act
328+
routeBuilder.MapGrpcService<GreeterServiceWithMetadataAttributes>();
329+
330+
// Assert
331+
var endpoints = routeBuilder.DataSources
332+
.SelectMany(ds => ds.Endpoints)
333+
.ToList();
334+
335+
Assert.IsNull(endpoints.SingleOrDefault(e => e.DisplayName == "gRPC - Unimplemented service"));
336+
Assert.IsNull(endpoints.SingleOrDefault(e => e.DisplayName == "gRPC - Unimplemented method for GreeterServiceWithMetadataAttributes"));
337+
}
338+
339+
[Test]
340+
public void MapGrpcService_IgnoreUnknownServicesServiceTrue_DontRegisterUnknownHandler()
341+
{
342+
// Arrange
343+
var services = ServicesHelpers.CreateServices(configureGrpcService: o =>
344+
{
345+
o.AddServiceOptions<GreeterServiceWithMetadataAttributes>(o => o.IgnoreUnknownServices = true);
346+
});
347+
348+
var routeBuilder = CreateTestEndpointRouteBuilder(services.BuildServiceProvider(validateScopes: true));
349+
350+
// Act
351+
routeBuilder.MapGrpcService<GreeterServiceWithMetadataAttributes>();
352+
353+
// Assert
354+
var endpoints = routeBuilder.DataSources
355+
.SelectMany(ds => ds.Endpoints)
356+
.ToList();
357+
358+
Assert.IsNotNull(endpoints.SingleOrDefault(e => e.DisplayName == "gRPC - Unimplemented service"));
359+
Assert.IsNull(endpoints.SingleOrDefault(e => e.DisplayName == "gRPC - Unimplemented method for GreeterServiceWithMetadataAttributes"));
360+
}
361+
299362
public IEndpointRouteBuilder CreateTestEndpointRouteBuilder(IServiceProvider serviceProvider)
300363
{
301364
return new TestEndpointRouteBuilder(serviceProvider);

test/Shared/ServicesHelpers.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@ namespace Grpc.Tests.Shared
2424
{
2525
internal static class ServicesHelpers
2626
{
27-
public static ServiceCollection CreateServices(Action<GrpcServiceOptions>? configureGrpc = null)
27+
public static ServiceCollection CreateServices(
28+
Action<GrpcServiceOptions>? configureGrpc = null,
29+
Action<IGrpcServerBuilder>? configureGrpcService = null)
2830
{
2931
var services = new ServiceCollection();
32+
3033
services.AddLogging();
31-
services.AddGrpc(configureGrpc ?? (o => { }));
34+
35+
var serverBuilder = services.AddGrpc(configureGrpc ?? (o => { }));
36+
configureGrpcService?.Invoke(serverBuilder);
37+
3238
return services;
3339
}
3440
}

0 commit comments

Comments
 (0)