diff --git a/README.md b/README.md index 8f94dd1c..dd5f1043 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,8 @@ Ocelot's primary functionality is to take incomeing http requests and forward th to a downstream service. At the moment in the form of another http request (in the future this could be any transport mechanism.). +Ocelot always adds a trailing slash to an UpstreamTemplate. + Ocelot's describes the routing of one request to another as a ReRoute. In order to get anything working in Ocelot you need to set up a ReRoute in the configuration. diff --git a/configuration-explanation.txt b/configuration-explanation.txt index c2ae4864..898be89f 100644 --- a/configuration-explanation.txt +++ b/configuration-explanation.txt @@ -1,9 +1,11 @@ { "ReRoutes": [ { - # The url we are forwarding the request to - "UpstreamTemplate": "/identityserverexample", - # The path we are listening on for this re route + # The url we are forwarding the request to, ocelot will not add a trailing slash + "DownstreamTemplate": "http://somehost.com/identityserverexample", + # The path we are listening on for this re route, Ocelot will add a trailing slash to + # this property. Then when a request is made Ocelot makes sure a trailing slash is added + # to that so everything matches "UpstreamTemplate": "/identityserverexample", # The method we are listening for on this re route "UpstreamHttpMethod": "Get", diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 51578756..f74f880b 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -7,6 +7,7 @@ using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; using Ocelot.Responses; +using Ocelot.Utilities; namespace Ocelot.Configuration.Creator { @@ -90,7 +91,6 @@ namespace Ocelot.Configuration.Creator ? globalConfiguration.RequestIdKey : reRoute.RequestIdKey; - if (isAuthenticated) { var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, @@ -120,6 +120,8 @@ namespace Ocelot.Configuration.Creator { var upstreamTemplate = reRoute.UpstreamTemplate; + upstreamTemplate = upstreamTemplate.SetLastCharacterAs('/'); + var placeholders = new List(); for (var i = 0; i < upstreamTemplate.Length; i++) @@ -138,9 +140,11 @@ namespace Ocelot.Configuration.Creator upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); } - return reRoute.ReRouteIsCaseSensitive + var route = reRoute.ReRouteIsCaseSensitive ? $"{upstreamTemplate}{RegExMatchEndString}" : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; + + return route; } private List GetAddThingsToRequest(Dictionary thingBeingAdded) diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 1aef7c7c..a74b6316 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -5,6 +5,7 @@ using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Utilities; namespace Ocelot.DownstreamRouteFinder.Middleware { @@ -29,7 +30,7 @@ namespace Ocelot.DownstreamRouteFinder.Middleware { _logger.LogDebug("started calling downstream route finder middleware"); - var upstreamUrlPath = context.Request.Path.ToString(); + var upstreamUrlPath = context.Request.Path.ToString().SetLastCharacterAs('/'); _logger.LogDebug("upstream url path is {upstreamUrlPath}", upstreamUrlPath); diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 645652f4..fc3ab200 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -46,14 +47,16 @@ namespace Ocelot.Errors.Middleware _logger.LogDebug("ocelot pipeline finished"); } - private static async Task SetInternalServerErrorOnResponse(HttpContext context) + private async Task SetInternalServerErrorOnResponse(HttpContext context) { - context.Response.StatusCode = 500; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync("Internal Server Error"); + context.Response.OnStarting(x => + { + context.Response.StatusCode = 500; + return Task.CompletedTask; + }, context); } - private static string CreateMessage(HttpContext context, Exception e) + private string CreateMessage(HttpContext context, Exception e) { var message = $"Exception caught in global error handler, exception message: {e.Message}, exception stack: {e.StackTrace}"; diff --git a/src/Ocelot/Utilities/StringExtensions.cs b/src/Ocelot/Utilities/StringExtensions.cs new file mode 100644 index 00000000..4655a98a --- /dev/null +++ b/src/Ocelot/Utilities/StringExtensions.cs @@ -0,0 +1,17 @@ +namespace Ocelot.Utilities +{ + public static class StringExtensions + { + public static string SetLastCharacterAs(this string valueToSetLastChar, + char expectedLastChar) + { + var last = valueToSetLastChar[valueToSetLastChar.Length - 1]; + + if (last != expectedLastChar) + { + valueToSetLastChar = $"{valueToSetLastChar}{expectedLastChar}"; + } + return valueToSetLastChar; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 57b8fa07..84e00d5b 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -56,6 +56,80 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_not_care_about_no_trailing() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/products", + UpstreamTemplate = "/products/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_not_care_about_trailing() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/products", + UpstreamTemplate = "/products", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_not_found() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/products", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + [Fact] public void should_return_response_200_with_complex_url() { diff --git a/test/Ocelot.ManualTest/configuration.json b/test/Ocelot.ManualTest/configuration.json index c1aa8b44..15276d6e 100644 --- a/test/Ocelot.ManualTest/configuration.json +++ b/test/Ocelot.ManualTest/configuration.json @@ -137,6 +137,12 @@ "UpstreamTemplate": "/customers/{customerId}", "UpstreamHttpMethod": "Delete", "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts", + "UpstreamTemplate": "/posts/", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } } ], "GlobalConfiguration": { diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index ba1b7569..4a4a5345 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -59,7 +59,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("(?i)/api/products/.*$") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") .Build() })) .BDDfy(); @@ -88,7 +88,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("(?i)/api/products/.*$") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") .Build() })) .BDDfy(); @@ -118,7 +118,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/api/products/.*$") + .WithUpstreamTemplatePattern("/api/products/.*/$") .Build() })) .BDDfy(); @@ -152,7 +152,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/api/products/.*$") + .WithUpstreamTemplatePattern("/api/products/.*/$") .WithRequestIdKey("blahhhh") .Build() })) @@ -183,7 +183,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/api/products/.*$") + .WithUpstreamTemplatePattern("/api/products/.*/$") .Build() })) .BDDfy(); @@ -198,7 +198,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/api/products/.*$") + .WithUpstreamTemplatePattern("/api/products/.*/$") .WithAuthenticationProvider("IdentityServer") .WithAuthenticationProviderUrl("http://localhost:51888") .WithRequireHttps(false) @@ -261,7 +261,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/api/products/.*$") + .WithUpstreamTemplatePattern("/api/products/.*/$") .WithAuthenticationProvider("IdentityServer") .WithAuthenticationProviderUrl("http://localhost:51888") .WithRequireHttps(false) @@ -323,7 +323,7 @@ namespace Ocelot.UnitTests.Configuration .WithDownstreamTemplate("/products/{productId}") .WithUpstreamTemplate("/api/products/{productId}/variants/{variantId}") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/api/products/.*/variants/.*$") + .WithUpstreamTemplatePattern("/api/products/.*/variants/.*/$") .Build() })) .BDDfy(); diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index dc94288b..ea069593 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -148,7 +148,6 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher .BDDfy(); } - private void GivenIHaveAUpstreamPath(string downstreamPath) { _downstreamUrlPath = downstreamPath;