diff --git a/eFormAPI/eFormAPI.Web/Hosting/DeduplicateUnimplementedGrpcMatcherPolicy.cs b/eFormAPI/eFormAPI.Web/Hosting/DeduplicateUnimplementedGrpcMatcherPolicy.cs new file mode 100644 index 0000000000..39cbcc0ac9 --- /dev/null +++ b/eFormAPI/eFormAPI.Web/Hosting/DeduplicateUnimplementedGrpcMatcherPolicy.cs @@ -0,0 +1,95 @@ +/* +The MIT License (MIT) + +Copyright (c) 2007 - 2021 Microting A/S + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; + +namespace eFormAPI.Web.Hosting; + +/// +/// When multiple plugins call services.AddGrpc() (each plugin needs +/// its own call so its MapGrpcService<T> validation passes), +/// each registration adds a catch-all "Unimplemented service" endpoint +/// to the host's shared endpoint table. Three plugins => three duplicates => +/// AmbiguousMatchException on any unmapped gRPC route. +/// +/// This policy runs at endpoint selection time and invalidates all but +/// one duplicate so the request gets a clean gRPC UNIMPLEMENTED +/// status (status code 12) instead of HTTP 500. +/// +/// The DisplayName string ("gRPC - Unimplemented service") is set by +/// Grpc.AspNetCore.Server in +/// ServiceRouteBuilder.CreateUnimplementedEndpoint. +/// +public sealed class DeduplicateUnimplementedGrpcMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy +{ + private const string UnimplementedMarker = "Unimplemented service"; + + public override int Order => -100; + + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + var seen = false; + foreach (var endpoint in endpoints) + { + if (IsUnimplemented(endpoint)) + { + if (seen) return true; + seen = true; + } + } + return false; + } + + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + var keptOne = false; + for (var i = 0; i < candidates.Count; i++) + { + if (!candidates.IsValidCandidate(i)) continue; + var endpoint = candidates[i].Endpoint; + if (!IsUnimplemented(endpoint)) continue; + + if (keptOne) + { + candidates.SetValidity(i, false); + } + else + { + keptOne = true; + } + } + return Task.CompletedTask; + } + + private static bool IsUnimplemented(Endpoint endpoint) + { + var displayName = endpoint?.DisplayName; + return displayName != null && displayName.Contains(UnimplementedMarker, StringComparison.Ordinal); + } +} diff --git a/eFormAPI/eFormAPI.Web/Startup.cs b/eFormAPI/eFormAPI.Web/Startup.cs index 534e3d6531..b118cbc108 100644 --- a/eFormAPI/eFormAPI.Web/Startup.cs +++ b/eFormAPI/eFormAPI.Web/Startup.cs @@ -38,8 +38,10 @@ namespace eFormAPI.Web; using Abstractions.Advanced; using Abstractions.Eforms; using Abstractions.Security; +using Hosting; using Hosting.Extensions; using Hosting.Security; +using Microsoft.AspNetCore.Routing; using Infrastructure.Models.Settings.Plugins; using Services; using Services.Security; @@ -323,6 +325,11 @@ public void ConfigureServices(IServiceCollection services) // gRPC services.AddGrpc(); + // Each plugin's AddGrpc() call registers a duplicate "Unimplemented service" + // fallback endpoint. Multiple duplicates cause AmbiguousMatchException on any + // unmapped gRPC route. This policy keeps a single fallback at routing time. + services.AddSingleton(); + // plugins services.AddEFormPlugins(Program.EnabledPlugins); }