From 617cec1ea5f68db328f061b05323aacff1e68b0d Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Thu, 16 Nov 2017 17:20:07 +0000 Subject: [PATCH 1/2] Add 405 handler --- src/BotwinExtensions.cs | 35 ++++++++++++++++++++++++++++++++++- test/BotwinModuleTests.cs | 17 +++++++++++++---- test/TestModule.cs | 3 +++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/BotwinExtensions.cs b/src/BotwinExtensions.cs index 03359a88..427949ef 100644 --- a/src/BotwinExtensions.cs +++ b/src/BotwinExtensions.cs @@ -1,6 +1,7 @@ namespace Botwin { using System; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; @@ -39,8 +40,12 @@ public static IApplicationBuilder UseBotwin(this IApplicationBuilder builder, Bo //Create a "startup scope" to resolve modules from using (var scope = builder.ApplicationServices.CreateScope()) { + var modules = scope.ServiceProvider.GetServices(); + + Apply405Handler(builder, modules); + //Get all instances of BotwinModule to fetch and register declared routes - foreach (var module in scope.ServiceProvider.GetServices()) + foreach (var module in modules) { var moduleType = module.GetType(); @@ -54,6 +59,34 @@ public static IApplicationBuilder UseBotwin(this IApplicationBuilder builder, Bo return builder.UseRouter(routeBuilder.Build()); } + private static void Apply405Handler(IApplicationBuilder builder, IEnumerable modules) + { + var systemRoutes = new List<(string verb, string route)>(); + foreach (var module in modules) + { + foreach (var route in module.Routes.Keys) + { + var strippedPath = route.path.EndsWith("/") ? route.path.Substring(0, route.path.Length - 1) : route.path; + systemRoutes.Add((route.verb, "/" + strippedPath)); + } + } + + builder.Use(async (context, next) => + { + var strippedPath = context.Request.Path.Value.EndsWith("/") && context.Request.Path.Value.Length > 1 + ? context.Request.Path.Value.Substring(0, context.Request.Path.Value.Length - 1) + : context.Request.Path.Value; + + var verbsForPath = systemRoutes.Where(x => x.route == strippedPath).Select(y => y.verb); + if (verbsForPath.All(x => x != context.Request.Method)) + { + context.Response.StatusCode = 405; + return; + } + await next(); + }); + } + private static RequestDelegate CreateRouteHandler((string verb, string path) route, Type moduleType) { return async ctx => diff --git a/test/BotwinModuleTests.cs b/test/BotwinModuleTests.cs index 15554132..298349a7 100755 --- a/test/BotwinModuleTests.cs +++ b/test/BotwinModuleTests.cs @@ -16,10 +16,7 @@ public class BotwinModuleTests public BotwinModuleTests() { this.server = new TestServer(new WebHostBuilder() - .ConfigureServices(x => - { - x.AddBotwin(typeof(TestModule).GetTypeInfo().Assembly); - }) + .ConfigureServices(x => { x.AddBotwin(typeof(TestModule).GetTypeInfo().Assembly); }) .Configure(x => x.UseBotwin()) ); this.httpClient = this.server.CreateClient(); @@ -261,5 +258,17 @@ public async Task Should_return_GET_requests_with_parsed_quersytring_with_defaul Assert.Equal(200, (int)response.StatusCode); Assert.True(body.Contains("Managed to parse default int 69")); } + + [Theory] + [InlineData("/405test")] + [InlineData("/405test/")] + [InlineData("/405testwithslash")] + [InlineData("/405testwithslash/")] + public async Task Should_return_405_if_path_not_found_for_supplied_method(string path) + { + var response = await this.httpClient.PostAsync(path, new StringContent("")); + + Assert.Equal(405, (int)response.StatusCode); + } } } diff --git a/test/TestModule.cs b/test/TestModule.cs index 687f0fdf..35732019 100644 --- a/test/TestModule.cs +++ b/test/TestModule.cs @@ -59,6 +59,9 @@ public TestModule() await ctx.Response.WriteAsync(content); }); + this.Get("405test", context => context.Response.WriteAsync("hi")); + this.Get("405testwithslash/", context => context.Response.WriteAsync("hi")); + this.Post("/", async (ctx) => { await ctx.Response.WriteAsync("Hello"); }); this.Put("/", async (ctx) => { await ctx.Response.WriteAsync("Hello"); }); this.Delete("/", async (ctx) => { await ctx.Response.WriteAsync("Hello"); }); From bcbd2bf36fd54c95da33caba01315076908aac96 Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Fri, 17 Nov 2017 08:23:42 +0000 Subject: [PATCH 2/2] added 405 handler as an after handler via @poke comments --- src/BotwinExtensions.cs | 48 +++++++++++++++++++++------------------ test/BotwinModuleTests.cs | 8 +++++++ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/BotwinExtensions.cs b/src/BotwinExtensions.cs index 427949ef..ba441ac9 100644 --- a/src/BotwinExtensions.cs +++ b/src/BotwinExtensions.cs @@ -5,6 +5,7 @@ namespace Botwin using System.IO; using System.Linq; using System.Reflection; + using System.Threading.Tasks; using FluentValidation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -36,14 +37,13 @@ public static IApplicationBuilder UseBotwin(this IApplicationBuilder builder, Bo ApplyGlobalAfterHook(builder, options); var routeBuilder = new RouteBuilder(builder); + var systemRoutes = new List<(string verb, string route)>(); //Create a "startup scope" to resolve modules from using (var scope = builder.ApplicationServices.CreateScope()) { var modules = scope.ServiceProvider.GetServices(); - Apply405Handler(builder, modules); - //Get all instances of BotwinModule to fetch and register declared routes foreach (var module in modules) { @@ -52,39 +52,43 @@ public static IApplicationBuilder UseBotwin(this IApplicationBuilder builder, Bo foreach (var route in module.Routes.Keys) { routeBuilder.MapVerb(route.verb, route.path, CreateRouteHandler(route, moduleType)); + + var strippedPath = route.path.EndsWith("/") ? route.path.Substring(0, route.path.Length - 1) : route.path; + systemRoutes.Add((route.verb, "/" + strippedPath)); } } - } - return builder.UseRouter(routeBuilder.Build()); + builder.UseRouter(routeBuilder.Build()); + + return builder.Use((ctx, next) => GetMethodNotAllowedHandler(ctx, next, systemRoutes)); + } } - private static void Apply405Handler(IApplicationBuilder builder, IEnumerable modules) + /// + /// This method is only called if it's a valid 404 or 405 + /// + /// + /// + /// + /// + private static async Task GetMethodNotAllowedHandler(HttpContext context, Func next, IEnumerable<(string verb, string route)> systemRoutes) { - var systemRoutes = new List<(string verb, string route)>(); - foreach (var module in modules) - { - foreach (var route in module.Routes.Keys) - { - var strippedPath = route.path.EndsWith("/") ? route.path.Substring(0, route.path.Length - 1) : route.path; - systemRoutes.Add((route.verb, "/" + strippedPath)); - } - } + //Call the final pipeline which gets ASP.Net Core status code, usually a 404 in this case + await next(); - builder.Use(async (context, next) => - { - var strippedPath = context.Request.Path.Value.EndsWith("/") && context.Request.Path.Value.Length > 1 - ? context.Request.Path.Value.Substring(0, context.Request.Path.Value.Length - 1) - : context.Request.Path.Value; + var strippedPath = context.Request.Path.Value.EndsWith("/") && context.Request.Path.Value.Length > 1 + ? context.Request.Path.Value.Substring(0, context.Request.Path.Value.Length - 1) + : context.Request.Path.Value; + //ASP.Net Core will set a 405 response to 404. Let's check if it's a valid 404 first otherwise if we know about the route it's most likely a 405 + if (context.Response.StatusCode == 404 && systemRoutes.Any(x => x.route == strippedPath)) + { var verbsForPath = systemRoutes.Where(x => x.route == strippedPath).Select(y => y.verb); if (verbsForPath.All(x => x != context.Request.Method)) { context.Response.StatusCode = 405; - return; } - await next(); - }); + } } private static RequestDelegate CreateRouteHandler((string verb, string path) route, Type moduleType) diff --git a/test/BotwinModuleTests.cs b/test/BotwinModuleTests.cs index 298349a7..507fe6b4 100755 --- a/test/BotwinModuleTests.cs +++ b/test/BotwinModuleTests.cs @@ -270,5 +270,13 @@ public async Task Should_return_405_if_path_not_found_for_supplied_method(string Assert.Equal(405, (int)response.StatusCode); } + + [Fact] + public async Task Should_return_404_if_path_not_found() + { + var response = await this.httpClient.GetAsync("/flibbertygibbert"); + + Assert.Equal(404, (int)response.StatusCode); + } } }