diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst index 5063c1b7..6a333141 100644 --- a/docs/features/headerstransformation.rst +++ b/docs/features/headerstransformation.rst @@ -3,8 +3,32 @@ Headers Transformation Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 `_ and I decided that it was going to be useful in various ways. -Syntax -^^^^^^ +Add to Response +^^^^^^^^^^^^^^^ + +This feature was requested in `GitHub #280 `_. I have only implemented +for responses but could add for requests in the future. + +If you want to add a header to your downstream response please add the following to a ReRoute in configuration.json.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Uncle": "Bob" + }, + +In the example above a header with the key Uncle and value Bob would be returned by Ocelot when requesting the specific ReRoute. + +If you want to return the Butterfly APM trace id then do something like the following.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "AnyKey": "{TraceId}" + }, + +Find and Replace +^^^^^^^^^^^^^^^^ In order to transform a header first we specify the header key and then the type of transform we want e.g. @@ -43,6 +67,7 @@ Ocelot allows placeholders that can be used in header transformation. {BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. {DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment. +{TraceId} - This will use the Butterfly APM Trace Id. This only works for DownstreamHeaderTransform at the moment. Handling 302 Redirects ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs index 0e28aaa7..d6dc0f34 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net.Http; using Ocelot.Values; using System.Linq; +using Ocelot.Configuration.Creator; namespace Ocelot.Configuration.Builder { @@ -37,11 +38,13 @@ namespace Ocelot.Configuration.Builder private string _upstreamHost; private string _key; private List _delegatingHandlers; + private List _addHeadersToDownstream; public DownstreamReRouteBuilder() { _downstreamAddresses = new List(); _delegatingHandlers = new List(); + _addHeadersToDownstream = new List(); } public DownstreamReRouteBuilder WithDownstreamAddresses(List downstreamAddresses) @@ -224,6 +227,12 @@ namespace Ocelot.Configuration.Builder return this; } + public DownstreamReRouteBuilder WithAddHeadersToDownstream(List addHeadersToDownstream) + { + _addHeadersToDownstream = addHeadersToDownstream; + return this; + } + public DownstreamReRoute Build() { return new DownstreamReRoute( @@ -253,7 +262,8 @@ namespace Ocelot.Configuration.Builder _authenticationOptions, new PathTemplate(_downstreamPathTemplate), _reRouteKey, - _delegatingHandlers); + _delegatingHandlers, + _addHeadersToDownstream); } } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index 94cde717..1036f8df 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -213,6 +213,7 @@ namespace Ocelot.Configuration.Creator .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) .WithUpstreamHost(fileReRoute.UpstreamHost) .WithDelegatingHandlers(fileReRoute.DelegatingHandlers) + .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) .Build(); return reRoute; diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index 7ab33e90..4ff87d2f 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -1,20 +1,22 @@ using System; using System.Collections.Generic; using Ocelot.Configuration.File; +using Ocelot.Infrastructure; +using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Responses; namespace Ocelot.Configuration.Creator { public class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator { - private IBaseUrlFinder _finder; - private readonly Dictionary> _placeholders; + private IPlaceholders _placeholders; + private IOcelotLogger _logger; - public HeaderFindAndReplaceCreator(IBaseUrlFinder finder) + public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFactory factory) { - _finder = finder; - _placeholders = new Dictionary>(); - _placeholders.Add("{BaseUrl}", () => _finder.Find()); + _logger = factory.CreateLogger();; + _placeholders = placeholders; } public HeaderTransformations Create(FileReRoute fileReRoute) @@ -24,21 +26,43 @@ namespace Ocelot.Configuration.Creator foreach(var input in fileReRoute.UpstreamHeaderTransform) { var hAndr = Map(input); - upstream.Add(hAndr); + if(!hAndr.IsError) + { + upstream.Add(hAndr.Data); + } + else + { + _logger.LogError($"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); + } } var downstream = new List(); + var addHeadersToDownstream = new List(); foreach(var input in fileReRoute.DownstreamHeaderTransform) { - var hAndr = Map(input); - downstream.Add(hAndr); + if(input.Value.Contains(",")) + { + var hAndr = Map(input); + if(!hAndr.IsError) + { + downstream.Add(hAndr.Data); + } + else + { + _logger.LogError($"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); + } + } + else + { + addHeadersToDownstream.Add(new AddHeader(input.Key, input.Value)); + } } - return new HeaderTransformations(upstream, downstream); + return new HeaderTransformations(upstream, downstream, addHeadersToDownstream); } - private HeaderFindAndReplace Map(KeyValuePair input) + private Response Map(KeyValuePair input) { var findAndReplace = input.Value.Split(","); @@ -51,16 +75,19 @@ namespace Ocelot.Configuration.Creator var placeholder = replace.Substring(startOfPlaceholder, startOfPlaceholder + (endOfPlaceholder + 1)); - if(_placeholders.ContainsKey(placeholder)) + var value = _placeholders.Get(placeholder); + + if(value.IsError) { - var value = _placeholders[placeholder].Invoke(); - replace = replace.Replace(placeholder, value); + return new ErrorResponse(value.Errors); } + + replace = replace.Replace(placeholder, value.Data); } var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0); - return hAndr; + return new OkResponse(hAndr); } } } diff --git a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs index 55e1e3b9..72d307e5 100644 --- a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs +++ b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs @@ -4,14 +4,31 @@ namespace Ocelot.Configuration.Creator { public class HeaderTransformations { - public HeaderTransformations(List upstream, List downstream) + public HeaderTransformations( + List upstream, + List downstream, + List addHeader) { + AddHeadersToDownstream = addHeader; Upstream = upstream; Downstream = downstream; } - public List Upstream {get;private set;} + public List Upstream { get; private set; } - public List Downstream {get;private set;} + public List Downstream { get; private set; } + public List AddHeadersToDownstream {get;private set;} + } + + public class AddHeader + { + public AddHeader(string key, string value) + { + this.Key = key; + this.Value = value; + + } + public string Key { get; private set; } + public string Value { get; private set; } } } diff --git a/src/Ocelot/Configuration/DownstreamReRoute.cs b/src/Ocelot/Configuration/DownstreamReRoute.cs index 6c9aa1dd..00accc07 100644 --- a/src/Ocelot/Configuration/DownstreamReRoute.cs +++ b/src/Ocelot/Configuration/DownstreamReRoute.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Ocelot.Configuration.Creator; using Ocelot.Values; namespace Ocelot.Configuration @@ -32,8 +33,10 @@ namespace Ocelot.Configuration AuthenticationOptions authenticationOptions, PathTemplate downstreamPathTemplate, string reRouteKey, - List delegatingHandlers) + List delegatingHandlers, + List addHeadersToDownstream) { + AddHeadersToDownstream = addHeadersToDownstream; DelegatingHandlers = delegatingHandlers; Key = key; UpstreamPathTemplate = upstreamPathTemplate; @@ -90,5 +93,6 @@ namespace Ocelot.Configuration public PathTemplate DownstreamPathTemplate { get; private set; } public string ReRouteKey { get; private set; } public List DelegatingHandlers {get;private set;} + public List AddHeadersToDownstream {get;private set;} } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index eccab083..8ccb1459 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -52,6 +52,7 @@ namespace Ocelot.DependencyInjection using System.Linq; using System.Net.Http; using Butterfly.Client.AspNetCore; + using Ocelot.Infrastructure; public class OcelotBuilder : IOcelotBuilder { @@ -149,6 +150,8 @@ namespace Ocelot.DependencyInjection // We add this here so that we can always inject something into the factory for IoC.. _services.AddSingleton(); _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); } public IOcelotAdministrationBuilder AddAdministration(string path, string secret) diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index b976e29c..ec44ae23 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -34,6 +34,7 @@ RateLimitOptionsError, PathTemplateDoesntStartWithForwardSlash, FileValidationFailedError, - UnableToFindDelegatingHandlerProviderError + UnableToFindDelegatingHandlerProviderError, + CouldNotFindPlaceholderError } } diff --git a/src/Ocelot/Headers/AddHeadersToRequest.cs b/src/Ocelot/Headers/AddHeadersToRequest.cs index c8ec8cf3..f61b1ac3 100644 --- a/src/Ocelot/Headers/AddHeadersToRequest.cs +++ b/src/Ocelot/Headers/AddHeadersToRequest.cs @@ -4,6 +4,7 @@ using Ocelot.Configuration; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Responses; using System.Net.Http; +using Ocelot.Configuration.Creator; namespace Ocelot.Headers { diff --git a/src/Ocelot/Headers/AddHeadersToResponse.cs b/src/Ocelot/Headers/AddHeadersToResponse.cs new file mode 100644 index 00000000..4c6f48ce --- /dev/null +++ b/src/Ocelot/Headers/AddHeadersToResponse.cs @@ -0,0 +1,44 @@ +namespace Ocelot.Headers +{ + using System; + using System.Collections.Generic; + using System.Net.Http; + using Ocelot.Configuration.Creator; + using Ocelot.Infrastructure; + using Ocelot.Infrastructure.RequestData; + using Ocelot.Logging; + + public class AddHeadersToResponse : IAddHeadersToResponse + { + private IPlaceholders _placeholders; + private IOcelotLogger _logger; + + public AddHeadersToResponse(IPlaceholders placeholders, IOcelotLoggerFactory factory) + { + _logger = factory.CreateLogger(); + _placeholders = placeholders; + } + public void Add(List addHeaders, HttpResponseMessage response) + { + foreach(var add in addHeaders) + { + if(add.Value.StartsWith('{') && add.Value.EndsWith('}')) + { + var value = _placeholders.Get(add.Value); + + if(value.IsError) + { + _logger.LogError($"Unable to add header to response {add.Key}: {add.Value}"); + continue; + } + + response.Headers.TryAddWithoutValidation(add.Key, value.Data); + } + else + { + response.Headers.TryAddWithoutValidation(add.Key, add.Value); + } + } + } + } +} diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs index cb45ffc3..fc3e8e4c 100644 --- a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using Ocelot.Configuration; +using Ocelot.Infrastructure; using Ocelot.Infrastructure.Extensions; using Ocelot.Responses; @@ -10,24 +11,14 @@ namespace Ocelot.Headers { public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer { - private Dictionary> _placeholders; + private IPlaceholders _placeholders; - public HttpResponseHeaderReplacer() + public HttpResponseHeaderReplacer(IPlaceholders placeholders) { - _placeholders = new Dictionary>(); - _placeholders.Add("{DownstreamBaseUrl}", x => { - var downstreamUrl = $"{x.RequestUri.Scheme}://{x.RequestUri.Host}"; - - if(x.RequestUri.Port != 80 && x.RequestUri.Port != 443) - { - downstreamUrl = $"{downstreamUrl}:{x.RequestUri.Port}"; - } - - return $"{downstreamUrl}/"; - }); + _placeholders = placeholders; } - public Response Replace(HttpResponseMessage response, List fAndRs, HttpRequestMessage httpRequestMessage) + public Response Replace(HttpResponseMessage response, List fAndRs, HttpRequestMessage request) { foreach (var f in fAndRs) { @@ -35,11 +26,13 @@ namespace Ocelot.Headers if(response.Headers.TryGetValues(f.Key, out var values)) { //check to see if it is a placeholder in the find... - if(_placeholders.TryGetValue(f.Find, out var replacePlaceholder)) + var placeholderValue = _placeholders.Get(f.Find, request); + + if(!placeholderValue.IsError) { //if it is we need to get the value of the placeholder - var find = replacePlaceholder(httpRequestMessage); - var replaced = values.ToList()[f.Index].Replace(find, f.Replace.LastCharAsForwardSlash()); + //var find = replacePlaceholder(httpRequestMessage); + var replaced = values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); response.Headers.Remove(f.Key); response.Headers.Add(f.Key, replaced); } diff --git a/src/Ocelot/Headers/IAddHeadersToRequest.cs b/src/Ocelot/Headers/IAddHeadersToRequest.cs index 387b3f01..fed2407c 100644 --- a/src/Ocelot/Headers/IAddHeadersToRequest.cs +++ b/src/Ocelot/Headers/IAddHeadersToRequest.cs @@ -4,6 +4,8 @@ using System.Net.Http; using Ocelot.Configuration; + using Ocelot.Configuration.Creator; + using Ocelot.Infrastructure.RequestData; using Ocelot.Responses; public interface IAddHeadersToRequest diff --git a/src/Ocelot/Headers/IAddHeadersToResponse.cs b/src/Ocelot/Headers/IAddHeadersToResponse.cs new file mode 100644 index 00000000..51f23758 --- /dev/null +++ b/src/Ocelot/Headers/IAddHeadersToResponse.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Headers +{ + using System.Collections.Generic; + using System.Net.Http; + using Ocelot.Configuration.Creator; + + public interface IAddHeadersToResponse + { + void Add(List addHeaders, HttpResponseMessage response); + } +} diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs index f4281a23..b09b40f8 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -13,12 +13,15 @@ namespace Ocelot.Headers.Middleware private readonly IOcelotLogger _logger; private readonly IHttpContextRequestHeaderReplacer _preReplacer; private readonly IHttpResponseHeaderReplacer _postReplacer; + private readonly IAddHeadersToResponse _addHeaders; public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpContextRequestHeaderReplacer preReplacer, - IHttpResponseHeaderReplacer postReplacer) + IHttpResponseHeaderReplacer postReplacer, + IAddHeadersToResponse addHeaders) { + _addHeaders = addHeaders; _next = next; _postReplacer = postReplacer; _preReplacer = preReplacer; @@ -37,6 +40,8 @@ namespace Ocelot.Headers.Middleware var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; _postReplacer.Replace(context.DownstreamResponse, postFAndRs, context.DownstreamRequest); + + _addHeaders.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); } } } diff --git a/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs new file mode 100644 index 00000000..c6214e83 --- /dev/null +++ b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Infrastructure +{ + public class CouldNotFindPlaceholderError : Error + { + public CouldNotFindPlaceholderError(string placeholder) + : base($"Unable to find placeholder called {placeholder}", OcelotErrorCode.CouldNotFindPlaceholderError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/IPlaceholders.cs b/src/Ocelot/Infrastructure/IPlaceholders.cs new file mode 100644 index 00000000..f95fb8b8 --- /dev/null +++ b/src/Ocelot/Infrastructure/IPlaceholders.cs @@ -0,0 +1,11 @@ +using System.Net.Http; +using Ocelot.Responses; + +namespace Ocelot.Infrastructure +{ + public interface IPlaceholders + { + Response Get(string key); + Response Get(string key, HttpRequestMessage request); + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/Placeholders.cs b/src/Ocelot/Infrastructure/Placeholders.cs new file mode 100644 index 00000000..b00f54fa --- /dev/null +++ b/src/Ocelot/Infrastructure/Placeholders.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Responses; + +namespace Ocelot.Infrastructure +{ + public class Placeholders : IPlaceholders + { + private Dictionary>> _placeholders; + private Dictionary> _requestPlaceholders; + private readonly IBaseUrlFinder _finder; + private readonly IRequestScopedDataRepository _repo; + + public Placeholders(IBaseUrlFinder finder, IRequestScopedDataRepository repo) + { + _repo = repo; + _finder = finder; + _placeholders = new Dictionary>>(); + _placeholders.Add("{BaseUrl}", () => new OkResponse(_finder.Find())); + _placeholders.Add("{TraceId}", () => { + var traceId = _repo.Get("TraceId"); + if(traceId.IsError) + { + return new ErrorResponse(traceId.Errors); + } + + return new OkResponse(traceId.Data); + }); + + _requestPlaceholders = new Dictionary>(); + _requestPlaceholders.Add("{DownstreamBaseUrl}", x => { + var downstreamUrl = $"{x.RequestUri.Scheme}://{x.RequestUri.Host}"; + + if(x.RequestUri.Port != 80 && x.RequestUri.Port != 443) + { + downstreamUrl = $"{downstreamUrl}:{x.RequestUri.Port}"; + } + + return $"{downstreamUrl}/"; + }); + } + + public Response Get(string key) + { + if(_placeholders.ContainsKey(key)) + { + var response = _placeholders[key].Invoke(); + if(!response.IsError) + { + return new OkResponse(response.Data); + } + } + + return new ErrorResponse(new CouldNotFindPlaceholderError(key)); + } + + public Response Get(string key, HttpRequestMessage request) + { + if(_requestPlaceholders.ContainsKey(key)) + { + return new OkResponse(_requestPlaceholders[key].Invoke(request)); + } + + return new ErrorResponse(new CouldNotFindPlaceholderError(key)); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs index 33bd4caf..9de364ac 100644 --- a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs +++ b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs @@ -5,26 +5,37 @@ using System.Threading; using System.Threading.Tasks; using Butterfly.Client.Tracing; using Butterfly.OpenTracing; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.Requester { public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler { private readonly IServiceTracer _tracer; + private readonly IRequestScopedDataRepository _repo; private const string prefix_spanId = "ot-spanId"; - public OcelotHttpTracingHandler(IServiceTracer tracer, HttpMessageHandler httpMessageHandler = null) + public OcelotHttpTracingHandler( + IServiceTracer tracer, + IRequestScopedDataRepository repo, + HttpMessageHandler httpMessageHandler = null) { + _repo = repo; _tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); InnerHandler = httpMessageHandler ?? new HttpClientHandler(); } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) { return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken)); } - protected virtual async Task TracingSendAsync(ISpan span, HttpRequestMessage request, CancellationToken cancellationToken) + protected virtual async Task TracingSendAsync( + ISpan span, + HttpRequestMessage request, + CancellationToken cancellationToken) { IEnumerable traceIdVals = null; if (request.Headers.TryGetValues(prefix_spanId, out traceIdVals)) @@ -33,6 +44,8 @@ namespace Ocelot.Requester request.Headers.TryAddWithoutValidation(prefix_spanId, span.SpanContext.SpanId); } + _repo.Add("TraceId", span.SpanContext.TraceId); + span.Tags.Client().Component("HttpClient") .HttpMethod(request.Method.Method) .HttpUrl(request.RequestUri.OriginalString) diff --git a/src/Ocelot/Requester/TracingHandlerFactory.cs b/src/Ocelot/Requester/TracingHandlerFactory.cs index 5cb72a79..b514ca18 100644 --- a/src/Ocelot/Requester/TracingHandlerFactory.cs +++ b/src/Ocelot/Requester/TracingHandlerFactory.cs @@ -1,20 +1,25 @@ using Butterfly.Client.Tracing; using Butterfly.OpenTracing; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.Requester { public class TracingHandlerFactory : ITracingHandlerFactory { private readonly IServiceTracer _tracer; + private readonly IRequestScopedDataRepository _repo; - public TracingHandlerFactory(IServiceTracer tracer) + public TracingHandlerFactory( + IServiceTracer tracer, + IRequestScopedDataRepository repo) { + _repo = repo; _tracer = tracer; } public ITracingHandler Get() { - return new OcelotHttpTracingHandler(_tracer); + return new OcelotHttpTracingHandler(_tracer, _repo); } } diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs index a7411ebe..d634e617 100644 --- a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -70,7 +70,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 51888, + Port = 51388, } }, UpstreamPathTemplate = "/api002/values", @@ -92,7 +92,7 @@ namespace Ocelot.AcceptanceTests var butterflyUrl = "http://localhost:9618"; this.Given(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl)) - .And(x => GivenServiceTwoIsRunning("http://localhost:51888", "/api/values", 200, "Hello from Tom", butterflyUrl)) + .And(x => GivenServiceTwoIsRunning("http://localhost:51388", "/api/values", 200, "Hello from Tom", butterflyUrl)) .And(x => GivenFakeButterfly(butterflyUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) @@ -109,6 +109,60 @@ namespace Ocelot.AcceptanceTests commandOnAllStateMachines.ShouldBeTrue(); } + [Fact] + public void should_return_tracing_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51387, + } + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + }, + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 3, + DurationOfBreak = 10, + TimeoutValue = 5000 + }, + DownstreamHeaderTransform = new Dictionary() + { + {"Trace-Id", "{TraceId}"}, + {"Tom", "Laura"} + } + } + } + }; + + var butterflyUrl = "http://localhost:9618"; + + this.Given(x => GivenServiceOneIsRunning("http://localhost:51387", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => GivenFakeButterfly(butterflyUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id")) + .And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura")) + .BDDfy(); + } + private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) { _serviceOneBuilder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 2cb0f830..1ee6f9bc 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -318,6 +318,12 @@ namespace Ocelot.AcceptanceTests header.First().ShouldBe(value); } + public void ThenTheTraceHeaderIsSet(string key) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldNotBeNullOrEmpty(); + } + public void GivenOcelotIsRunningUsingJsonSerializedCache() { _webHostBuilder = new WebHostBuilder(); diff --git a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs index 6499c4e0..7393b631 100644 --- a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs @@ -34,10 +34,10 @@ namespace Ocelot.UnitTests.Configuration _fileConfig = new FileConfiguration(); _config = new Mock(); _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); - _config.Setup(x => x.Delay).Returns(10); + _config.Setup(x => x.Delay).Returns(100); _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object, _config.Object); } - + public void Dispose() { _poller.Dispose(); diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 79aff9ed..ce1d6989 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -826,7 +826,8 @@ namespace Ocelot.UnitTests.Configuration result.DownstreamReRoute[0].ClaimsToHeaders.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToHeaders.Count); result.DownstreamReRoute[0].ClaimsToQueries.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToQueries.Count); result.DownstreamReRoute[0].RequestIdKey.ShouldBe(expected.DownstreamReRoute[0].RequestIdKey); - result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); + result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); + result.DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToDownstream); } } @@ -909,7 +910,7 @@ namespace Ocelot.UnitTests.Configuration private void GivenTheHeaderFindAndReplaceCreatorReturns() { - _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List())); + _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List(), new List())); } private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 37ee8957..aa285e8d 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -5,7 +5,11 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; +using Ocelot.Infrastructure; +using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -17,12 +21,17 @@ namespace Ocelot.UnitTests.Configuration private HeaderFindAndReplaceCreator _creator; private FileReRoute _reRoute; private HeaderTransformations _result; - private Mock _finder; + private Mock _placeholders; + private Mock _factory; + private Mock _logger; public HeaderFindAndReplaceCreatorTests() { - _finder = new Mock(); - _creator = new HeaderFindAndReplaceCreator(_finder.Object); + _logger = new Mock(); + _factory = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _placeholders = new Mock(); + _creator = new HeaderFindAndReplaceCreator(_placeholders.Object, _factory.Object); } [Fact] @@ -84,6 +93,40 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } + [Fact] + public void should_log_errors_and_not_add_headers() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + }, + UpstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + } + }; + + var expected = new List + { + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlErrors()) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingDownstreamIsReturned(expected)) + .And(x => ThenTheFollowingUpstreamIsReturned(expected)) + .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add DownstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) + .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add UpstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) + .BDDfy(); + } + + private void ThenTheLoggerIsCalledCorrectly(string message) + { + _logger.Verify(x => x.LogError(message), Times.Once); + } + [Fact] public void should_use_base_url_partial_placeholder() { @@ -107,9 +150,41 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } + + [Fact] + public void should_add_trace_id_header() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Trace-Id", "{TraceId}"}, + } + }; + + var expected = new AddHeader("Trace-Id", "{TraceId}"); + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingAddHeaderIsReturned(expected)) + .BDDfy(); + } + private void GivenTheBaseUrlIs(string baseUrl) { - _finder.Setup(x => x.Find()).Returns(baseUrl); + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(baseUrl)); + } + + private void GivenTheBaseUrlErrors() + { + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); + } + + private void ThenTheFollowingAddHeaderIsReturned(AddHeader addHeader) + { + _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key); + _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value); } private void ThenTheFollowingDownstreamIsReturned(List downstream) diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs new file mode 100644 index 00000000..a4a19eec --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -0,0 +1,148 @@ +using Xunit; +using Shouldly; +using TestStack.BDDfy; +using Ocelot.Headers; +using System.Net.Http; +using System.Collections.Generic; +using Ocelot.Configuration.Creator; +using System.Linq; +using Moq; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Responses; +using Ocelot.Infrastructure; +using Ocelot.UnitTests.Responder; +using System; +using Ocelot.Logging; + +namespace Ocelot.UnitTests.Headers +{ + public class AddHeadersToResponseTests + { + private IAddHeadersToResponse _adder; + private Mock _placeholders; + private HttpResponseMessage _response; + private List _addHeaders; + private Mock _factory; + private Mock _logger; + + public AddHeadersToResponseTests() + { + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _placeholders = new Mock(); + _adder = new AddHeadersToResponse(_placeholders.Object, _factory.Object); + } + + [Fact] + public void should_add_header() + { + var addHeaders = new List + { + new AddHeader("Laura", "Tom") + }; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .And(_ => ThenTheHeaderIsReturned("Laura", "Tom")) + .BDDfy(); + } + + [Fact] + public void should_add_trace_id_placeholder() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}") + }; + + var traceId = "123"; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdIs(traceId)) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsReturned("Trace-Id", traceId)) + .BDDfy(); + } + + [Fact] + public void should_add_trace_id_placeholder_and_normal() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}"), + new AddHeader("Tom", "Laura") + }; + + var traceId = "123"; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdIs(traceId)) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsReturned("Trace-Id", traceId)) + .Then(_ => ThenTheHeaderIsReturned("Tom", "Laura")) + .BDDfy(); + } + + [Fact] + public void should_do_nothing_and_log_error() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}") + }; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdErrors()) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsNotAdded("Trace-Id")) + .And(_ => ThenTheErrorIsLogged()) + .BDDfy(); + } + + private void ThenTheErrorIsLogged() + { + _logger.Verify(x => x.LogError("Unable to add header to response Trace-Id: {TraceId}"), Times.Once); + } + + private void ThenTheHeaderIsNotAdded(string key) + { + _response.Headers.TryGetValues(key, out var values).ShouldBeFalse(); + } + + private void GivenTheTraceIdIs(string traceId) + { + _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new OkResponse(traceId)); + } + + private void GivenTheTraceIdErrors() + { + _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new ErrorResponse(new AnyError())); + } + + private void ThenTheHeaderIsReturned(string key, string value) + { + var values = _response.Headers.GetValues(key); + values.First().ShouldBe(value); + } + + private void WhenIAdd() + { + _adder.Add(_addHeaders, _response); + } + + private void GivenAResponseMessage() + { + _response = new HttpResponseMessage(); + } + + private void GivenTheAddHeaders(List addHeaders) + { + _addHeaders = addHeaders; + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index 3085a64d..c49bf172 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -26,6 +26,7 @@ namespace Ocelot.UnitTests.Headers private HttpHeadersTransformationMiddleware _middleware; private DownstreamContext _downstreamContext; private OcelotRequestDelegate _next; + private Mock _addHeaders; public HttpHeadersTransformationMiddlewareTests() { @@ -36,7 +37,8 @@ namespace Ocelot.UnitTests.Headers _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _middleware = new HttpHeadersTransformationMiddleware(_next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object); + _addHeaders = new Mock(); + _middleware = new HttpHeadersTransformationMiddleware(_next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object, _addHeaders.Object); } [Fact] @@ -49,9 +51,16 @@ namespace Ocelot.UnitTests.Headers .When(x => WhenICallTheMiddleware()) .Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()) .And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()) + .And(x => ThenAddHeadersIsCalledCorrectly()) .BDDfy(); } + private void ThenAddHeadersIsCalledCorrectly() + { + _addHeaders + .Verify(x => x.Add(_downstreamContext.DownstreamReRoute.AddHeadersToDownstream, _downstreamContext.DownstreamResponse), Times.Once); + } + private void WhenICallTheMiddleware() { _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index 95ae6e6d..ca97de4f 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -7,20 +7,30 @@ using Ocelot.Configuration; using System.Collections.Generic; using Ocelot.Responses; using System.Linq; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Middleware; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.UnitTests.Headers { public class HttpResponseHeaderReplacerTests { private HttpResponseMessage _response; + private Placeholders _placeholders; private HttpResponseHeaderReplacer _replacer; private List _headerFindAndReplaces; private Response _result; private HttpRequestMessage _request; + private Mock _finder; + private Mock _repo; public HttpResponseHeaderReplacerTests() { - _replacer = new HttpResponseHeaderReplacer(); + _repo = new Mock(); + _finder = new Mock(); + _placeholders = new Placeholders(_finder.Object, _repo.Object); + _replacer = new HttpResponseHeaderReplacer(_placeholders); } [Fact] diff --git a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs new file mode 100644 index 00000000..e69de29b diff --git a/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs new file mode 100644 index 00000000..6acd3655 --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Net.Http; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Responses; +using Shouldly; +using Xunit; + +namespace Ocelot.UnitTests.Infrastructure +{ + public class PlaceholdersTests + { + private IPlaceholders _placeholders; + private Mock _finder; + private Mock _repo; + + public PlaceholdersTests() + { + _repo = new Mock(); + _finder = new Mock(); + _placeholders = new Placeholders(_finder.Object, _repo.Object); + } + + [Fact] + public void should_return_base_url() + { + var baseUrl = "http://www.bbc.co.uk"; + _finder.Setup(x => x.Find()).Returns(baseUrl); + var result = _placeholders.Get("{BaseUrl}"); + result.Data.ShouldBe(baseUrl); + } + + [Fact] + public void should_return_key_does_not_exist() + { + var result = _placeholders.Get("{Test}"); + result.IsError.ShouldBeTrue(); + result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); + } + + [Fact] + public void should_return_downstream_base_url_when_port_is_not_80_or_443() + { + var request = new HttpRequestMessage(); + request.RequestUri = new Uri("http://www.bbc.co.uk"); + var result = _placeholders.Get("{DownstreamBaseUrl}", request); + result.Data.ShouldBe("http://www.bbc.co.uk/"); + } + + + [Fact] + public void should_return_downstream_base_url_when_port_is_80_or_443() + { + var request = new HttpRequestMessage(); + request.RequestUri = new Uri("http://www.bbc.co.uk:123"); + var result = _placeholders.Get("{DownstreamBaseUrl}", request); + result.Data.ShouldBe("http://www.bbc.co.uk:123/"); + } + + [Fact] + public void should_return_key_does_not_exist_for_http_request_message() + { + var result = _placeholders.Get("{Test}", new System.Net.Http.HttpRequestMessage()); + result.IsError.ShouldBeTrue(); + result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); + } + + [Fact] + public void should_return_trace_id() + { + var traceId = "123"; + _repo.Setup(x => x.Get("TraceId")).Returns(new OkResponse(traceId)); + var result = _placeholders.Get("{TraceId}"); + result.Data.ShouldBe(traceId); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs index e8196966..bd633cb3 100644 --- a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs @@ -1,5 +1,6 @@ using Butterfly.Client.Tracing; using Moq; +using Ocelot.Infrastructure.RequestData; using Ocelot.Requester; using Shouldly; using Xunit; @@ -10,11 +11,13 @@ namespace Ocelot.UnitTests.Requester { private TracingHandlerFactory _factory; private Mock _tracer; + private Mock _repo; public TracingHandlerFactoryTests() { _tracer = new Mock(); - _factory = new TracingHandlerFactory(_tracer.Object); + _repo = new Mock(); + _factory = new TracingHandlerFactory(_tracer.Object, _repo.Object); } [Fact] diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 9800ffef..8ee2be1f 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -120,7 +120,7 @@ namespace Ocelot.UnitTests.Responder // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(33, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(34, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode)