make rate limiting whitelist a function so users can override with dynamic behaviour

* Make rate-limiting client whitelist dynamic
* Refactor `RateLimitOptions.ClientWhiteList`
* Fix typo in variable `enbleRateLimiting`
* Fix case in variable `clientIdheader`
author Taiwo Otubamowo <totubamowo@deloitte.co.uk>

* fix 1045

* #492 log 500 with error log level, acceptance test, unit test

* #492 minor changes

* initial commit for new feature #1077

allow to limit the number of concurrent tcp connection to a downstream service

* protect code against value not in accurate range

add unit test

* Do not crash host on Dispose

* Add test

* Pin GitVersion.CommandLine package version

* #683 validate if there are duplicated placeholders in UpstreamPathTemplate

* Use registered scheme from Eureka (#1087)

* extra test

* very brief mention MaxConnectionsPerServer in docs

* build develop like a PR

* more docs

* test

Co-authored-by: Taiwo O. <44668623+totubamowo@users.noreply.github.com>
Co-authored-by: Catcher Wong <catcher_hwq@outlook.com>
Co-authored-by: jlukawska <56401969+jlukawska@users.noreply.github.com>
Co-authored-by: buretjph <58700930+buretjph@users.noreply.github.com>
Co-authored-by: Jonathan Mezach <jonathanmezach@gmail.com>
Co-authored-by: 彭伟 <pengweiqhca@sina.com>
This commit is contained in:
Tom Pallister 2020-01-19 17:47:57 +00:00 committed by GitHub
parent 07df311094
commit ebe662abe6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 303 additions and 126 deletions

View File

@ -9,23 +9,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.dockerignore = .dockerignore .dockerignore = .dockerignore
.gitignore = .gitignore .gitignore = .gitignore
build-and-release-unstable.ps1 = build-and-release-unstable.ps1
build-and-run-tests.ps1 = build-and-run-tests.ps1
build.cake = build.cake build.cake = build.cake
build.ps1 = build.ps1 build.ps1 = build.ps1
codeanalysis.ruleset = codeanalysis.ruleset codeanalysis.ruleset = codeanalysis.ruleset
docker-compose.yaml = docker-compose.yaml
Dockerfile = Dockerfile
GitVersion.yml = GitVersion.yml GitVersion.yml = GitVersion.yml
global.json = global.json
LICENSE.md = LICENSE.md LICENSE.md = LICENSE.md
README.md = README.md README.md = README.md
release.ps1 = release.ps1
ReleaseNotes.md = ReleaseNotes.md ReleaseNotes.md = ReleaseNotes.md
run-acceptance-tests.ps1 = run-acceptance-tests.ps1
run-benchmarks.ps1 = run-benchmarks.ps1
run-unit-tests.ps1 = run-unit-tests.ps1
version.ps1 = version.ps1
EndProjectSection EndProjectSection
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}"

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:55029/",
"sslPort": 44390
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"OcelotBasic": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
namespace Ocelot.Configuration.Builder namespace Ocelot.Configuration.Builder
{ {
@ -7,6 +8,7 @@ namespace Ocelot.Configuration.Builder
private bool _enableRateLimiting; private bool _enableRateLimiting;
private string _clientIdHeader; private string _clientIdHeader;
private List<string> _clientWhitelist; private List<string> _clientWhitelist;
private Func<List<string>> _getClientWhitelist;
private bool _disableRateLimitHeaders; private bool _disableRateLimitHeaders;
private string _quotaExceededMessage; private string _quotaExceededMessage;
private string _rateLimitCounterPrefix; private string _rateLimitCounterPrefix;
@ -19,15 +21,15 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public RateLimitOptionsBuilder WithClientIdHeader(string clientIdheader) public RateLimitOptionsBuilder WithClientIdHeader(string clientIdHeader)
{ {
_clientIdHeader = clientIdheader; _clientIdHeader = clientIdHeader;
return this; return this;
} }
public RateLimitOptionsBuilder WithClientWhiteList(List<string> clientWhitelist) public RateLimitOptionsBuilder WithClientWhiteList(Func<List<string>> getClientWhitelist)
{ {
_clientWhitelist = clientWhitelist; _getClientWhitelist = getClientWhitelist;
return this; return this;
} }
@ -63,9 +65,9 @@ namespace Ocelot.Configuration.Builder
public RateLimitOptions Build() public RateLimitOptions Build()
{ {
return new RateLimitOptions(_enableRateLimiting, _clientIdHeader, _clientWhitelist, return new RateLimitOptions(_enableRateLimiting, _clientIdHeader, _getClientWhitelist,
_disableRateLimitHeaders, _quotaExceededMessage, _rateLimitCounterPrefix, _disableRateLimitHeaders, _quotaExceededMessage, _rateLimitCounterPrefix,
_rateLimitRule, _httpStatusCode); _rateLimitRule, _httpStatusCode);
} }
} }
} }

View File

@ -11,7 +11,7 @@ namespace Ocelot.Configuration.Creator
{ {
return new RateLimitOptionsBuilder() return new RateLimitOptionsBuilder()
.WithClientIdHeader(globalConfiguration.RateLimitOptions.ClientIdHeader) .WithClientIdHeader(globalConfiguration.RateLimitOptions.ClientIdHeader)
.WithClientWhiteList(fileRateLimitRule.ClientWhitelist) .WithClientWhiteList(() => fileRateLimitRule.ClientWhitelist)
.WithDisableRateLimitHeaders(globalConfiguration.RateLimitOptions.DisableRateLimitHeaders) .WithDisableRateLimitHeaders(globalConfiguration.RateLimitOptions.DisableRateLimitHeaders)
.WithEnableRateLimiting(fileRateLimitRule.EnableRateLimiting) .WithEnableRateLimiting(fileRateLimitRule.EnableRateLimiting)
.WithHttpStatusCode(globalConfiguration.RateLimitOptions.HttpStatusCode) .WithHttpStatusCode(globalConfiguration.RateLimitOptions.HttpStatusCode)

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
namespace Ocelot.Configuration namespace Ocelot.Configuration
{ {
@ -7,12 +8,14 @@ namespace Ocelot.Configuration
/// </summary> /// </summary>
public class RateLimitOptions public class RateLimitOptions
{ {
public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List<string> clientWhitelist, bool disableRateLimitHeaders, private readonly Func<List<string>> _getClientWhitelist;
public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func<List<string>> getClientWhitelist, bool disableRateLimitHeaders,
string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode) string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)
{ {
EnableRateLimiting = enbleRateLimiting; EnableRateLimiting = enableRateLimiting;
ClientIdHeader = clientIdHeader; ClientIdHeader = clientIdHeader;
ClientWhitelist = clientWhitelist ?? new List<string>(); _getClientWhitelist = getClientWhitelist;
DisableRateLimitHeaders = disableRateLimitHeaders; DisableRateLimitHeaders = disableRateLimitHeaders;
QuotaExceededMessage = quotaExceededMessage; QuotaExceededMessage = quotaExceededMessage;
RateLimitCounterPrefix = rateLimitCounterPrefix; RateLimitCounterPrefix = rateLimitCounterPrefix;
@ -22,18 +25,21 @@ namespace Ocelot.Configuration
public RateLimitRule RateLimitRule { get; private set; } public RateLimitRule RateLimitRule { get; private set; }
public List<string> ClientWhitelist { get; private set; } /// <summary>
/// Gets the list of white listed clients
/// </summary>
public List<string> ClientWhitelist { get => _getClientWhitelist(); }
/// <summary> /// <summary>
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
/// </summary> /// </summary>
public string ClientIdHeader { get; private set; } public string ClientIdHeader { get; private set; }
/// <summary> /// <summary>
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
/// </summary> /// </summary>
public int HttpStatusCode { get; private set; } public int HttpStatusCode { get; private set; }
/// <summary> /// <summary>
/// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message.
/// If none specified the default will be: /// If none specified the default will be:
@ -44,8 +50,8 @@ namespace Ocelot.Configuration
/// <summary> /// <summary>
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key /// Gets or sets the counter prefix, used to compose the rate limit counter cache key
/// </summary> /// </summary>
public string RateLimitCounterPrefix { get; private set; } public string RateLimitCounterPrefix { get; private set; }
/// <summary> /// <summary>
/// Enables endpoint rate limiting based URL path and HTTP verb /// Enables endpoint rate limiting based URL path and HTTP verb
/// </summary> /// </summary>
@ -56,4 +62,4 @@ namespace Ocelot.Configuration
/// </summary> /// </summary>
public bool DisableRateLimitHeaders { get; private set; } public bool DisableRateLimitHeaders { get; private set; }
} }
} }

View File

@ -1,6 +1,6 @@
namespace Ocelot.Requester namespace Ocelot.Requester
{ {
using Errors; using Ocelot.Errors;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -23,7 +23,7 @@ namespace Ocelot.Requester
return _mappers[type](exception); return _mappers[type](exception);
} }
if (type == typeof(OperationCanceledException)) if (type == typeof(OperationCanceledException) || type.IsSubclassOf(typeof(OperationCanceledException)))
{ {
return new RequestCanceledError(exception.Message); return new RequestCanceledError(exception.Message);
} }

View File

@ -1,6 +1,9 @@
using System.Net;
using System.Net.Http;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;
using System.Threading.Tasks; using System.Threading.Tasks;
using Ocelot.Responses;
namespace Ocelot.Requester.Middleware namespace Ocelot.Requester.Middleware
{ {
@ -22,6 +25,8 @@ namespace Ocelot.Requester.Middleware
{ {
var response = await _requester.GetResponse(context); var response = await _requester.GetResponse(context);
CreateLogBasedOnResponse(response);
if (response.IsError) if (response.IsError)
{ {
Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); Logger.LogDebug("IHttpRequester returned an error, setting pipeline error");
@ -36,5 +41,19 @@ namespace Ocelot.Requester.Middleware
await _next.Invoke(context); await _next.Invoke(context);
} }
private void CreateLogBasedOnResponse(Response<HttpResponseMessage> response)
{
if (response.Data?.StatusCode <= HttpStatusCode.BadRequest)
{
Logger.LogInformation(
$"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}");
}
else if (response.Data?.StatusCode >= HttpStatusCode.BadRequest)
{
Logger.LogWarning(
$"{(int) response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}");
}
}
} }
} }

View File

@ -1,74 +1,75 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<VersionPrefix>0.0.0-dev</VersionPrefix> <VersionPrefix>0.0.0-dev</VersionPrefix>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>Ocelot.AcceptanceTests</AssemblyName> <AssemblyName>Ocelot.AcceptanceTests</AssemblyName>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<PackageId>Ocelot.AcceptanceTests</PackageId> <PackageId>Ocelot.AcceptanceTests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<RuntimeIdentifiers>osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64</RuntimeIdentifiers> <RuntimeIdentifiers>osx.10.11-x64;osx.10.12-x64;win7-x64;win10-x64</RuntimeIdentifiers>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\..\codeanalysis.ruleset</CodeAnalysisRuleSet>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Update="appsettings.product.json"> <None Update="appsettings.product.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="appsettings.json"> <None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="idsrv3test.pfx"> <None Update="idsrv3test.pfx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj" /> <ProjectReference Include="..\..\src\Ocelot.Tracing.Butterfly\Ocelot.Tracing.Butterfly.csproj" />
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" /> <ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
<ProjectReference Include="..\..\src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj" /> <ProjectReference Include="..\..\src\Ocelot.Cache.CacheManager\Ocelot.Cache.CacheManager.csproj" />
<ProjectReference Include="..\..\src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj" /> <ProjectReference Include="..\..\src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj" />
<ProjectReference Include="..\..\src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj" /> <ProjectReference Include="..\..\src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj" />
<ProjectReference Include="..\..\src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj" /> <ProjectReference Include="..\..\src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj" />
<ProjectReference Include="..\Ocelot.ManualTest\Ocelot.ManualTest.csproj" /> <ProjectReference Include="..\Ocelot.ManualTest\Ocelot.ManualTest.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" /> <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.0.0" /> <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.66"> <PackageReference Include="Moq" Version="4.13.0" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.66">
</PackageReference> <PrivateAssets>all</PrivateAssets>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1"> </PackageReference>
<PrivateAssets>all</PrivateAssets> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.0" /> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.0.0" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="3.0.0" />
<PackageReference Include="Microsoft.DotNet.InternalAbstractions" Version="1.0.500-preview2-1-003177" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.0.0" />
<PackageReference Include="Shouldly" Version="4.0.0-beta0002" /> <PackageReference Include="Microsoft.DotNet.InternalAbstractions" Version="1.0.500-preview2-1-003177" />
<PackageReference Include="TestStack.BDDfy" Version="4.3.2" /> <PackageReference Include="Shouldly" Version="4.0.0-beta0002" />
<PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="TestStack.BDDfy" Version="4.3.2" />
<PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" /> <PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" /> <PackageReference Include="Butterfly.Client.AspNetCore" Version="0.0.8" />
<PackageReference Include="IdentityServer4" Version="3.0.1" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="Consul" Version="0.7.2.6" /> <PackageReference Include="IdentityServer4" Version="3.0.1" />
<PackageReference Include="Rafty" Version="0.4.4" /> <PackageReference Include="Consul" Version="0.7.2.6" />
<PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="2.0.0-beta-1629" /> <PackageReference Include="Rafty" Version="0.4.4" />
<PackageReference Include="CacheManager.Serialization.Json" Version="2.0.0-beta-1629" /> <PackageReference Include="CacheManager.Microsoft.Extensions.Logging" Version="2.0.0-beta-1629" />
<PackageReference Include="Pivotal.Discovery.ClientCore" Version="2.2.0" /> <PackageReference Include="CacheManager.Serialization.Json" Version="2.0.0-beta-1629" />
</ItemGroup> <PackageReference Include="Pivotal.Discovery.ClientCore" Version="2.2.0" />
<ItemGroup> </ItemGroup>
<PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" /> <ItemGroup>
</ItemGroup> <PackageReference Update="Microsoft.SourceLink.GitHub" Version="1.0.0" />
</ItemGroup>
</Project> </Project>

View File

@ -34,7 +34,7 @@ namespace Ocelot.AcceptanceTests
new FileHostAndPort new FileHostAndPort
{ {
Host = "localhost", Host = "localhost",
Port = 50092, Port = 51092,
} }
}, },
UpstreamPathTemplate = "/{everything}", UpstreamPathTemplate = "/{everything}",
@ -43,7 +43,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:50092", "/inline.132.bundle.js", 304)) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51092", "/inline.132.bundle.js", 304))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/inline.132.bundle.js")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/inline.132.bundle.js"))

View File

@ -20,7 +20,8 @@
[Fact] [Fact]
public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error() public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error()
{ {
var configuration = new FileConfiguration var configuration = new FileConfiguration
{ {
ReRoutes = new List<FileReRoute> ReRoutes = new List<FileReRoute>
@ -49,6 +50,39 @@
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError))
.BDDfy(); .BDDfy();
}
[Fact]
public void should_log_warning_if_downstream_service_returns_internal_server_error()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
DownstreamHostAndPorts = new List<FileHostAndPort>
{
new FileHostAndPort
{
Host = "localhost",
Port = 53876,
},
},
DownstreamScheme = "http",
},
},
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53876"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunningWithLogger())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenWarningShouldBeLogged())
.BDDfy();
} }
private void GivenThereIsAServiceRunningOn(string url) private void GivenThereIsAServiceRunningOn(string url)

View File

@ -10,12 +10,14 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Ocelot.Cache.CacheManager; using Ocelot.Cache.CacheManager;
using Ocelot.Configuration.Creator; using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.DependencyInjection; using Ocelot.DependencyInjection;
using Ocelot.Infrastructure; using Ocelot.Infrastructure;
using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;
using Ocelot.Middleware.Multiplexer; using Ocelot.Middleware.Multiplexer;
using Ocelot.Provider.Consul; using Ocelot.Provider.Consul;
@ -1120,5 +1122,60 @@
_ocelotClient = _ocelotServer.CreateClient(); _ocelotClient = _ocelotServer.CreateClient();
} }
public void GivenOcelotIsRunningWithLogger()
{
_webHostBuilder = new WebHostBuilder();
_webHostBuilder
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath);
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false);
config.AddJsonFile("ocelot.json", false, false);
config.AddEnvironmentVariables();
})
.ConfigureServices(s =>
{
s.AddOcelot();
s.AddSingleton<IOcelotLoggerFactory, MockLoggerFactory>();
})
.Configure(app =>
{
app.UseOcelot().Wait();
});
_ocelotServer = new TestServer(_webHostBuilder);
_ocelotClient = _ocelotServer.CreateClient();
}
public void ThenWarningShouldBeLogged()
{
MockLoggerFactory loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService<IOcelotLoggerFactory>();
loggerFactory.Verify();
}
internal class MockLoggerFactory : IOcelotLoggerFactory
{
private Mock<IOcelotLogger> _logger;
public IOcelotLogger CreateLogger<T>()
{
if (_logger == null)
{
_logger = new Mock<IOcelotLogger>();
_logger.Setup(x => x.LogWarning(It.IsAny<string>())).Verifiable();
}
return _logger.Object;
}
public void Verify()
{
_logger.Verify(x => x.LogWarning(It.IsAny<string>()), Times.Once);
}
}
} }
} }

View File

@ -50,7 +50,7 @@ namespace Ocelot.UnitTests.Configuration
}; };
var expected = new RateLimitOptionsBuilder() var expected = new RateLimitOptionsBuilder()
.WithClientIdHeader("ClientIdHeader") .WithClientIdHeader("ClientIdHeader")
.WithClientWhiteList(fileReRoute.RateLimitOptions.ClientWhitelist) .WithClientWhiteList(() => fileReRoute.RateLimitOptions.ClientWhitelist)
.WithDisableRateLimitHeaders(true) .WithDisableRateLimitHeaders(true)
.WithEnableRateLimiting(true) .WithEnableRateLimiting(true)
.WithHttpStatusCode(200) .WithHttpStatusCode(200)

View File

@ -49,15 +49,15 @@ namespace Ocelot.UnitTests.RateLimit
[Fact] [Fact]
public void should_call_middleware_and_ratelimiting() public void should_call_middleware_and_ratelimiting()
{ {
var upstreamTemplate = new UpstreamPathTemplateBuilder().Build(); var upstreamTemplate = new UpstreamPathTemplateBuilder().Build();
var downstreamReRoute = new DownstreamReRouteBuilder() var downstreamReRoute = new DownstreamReRouteBuilder()
.WithEnableRateLimiting(true) .WithEnableRateLimiting(true)
.WithRateLimitOptions(new RateLimitOptions(true, "ClientId", new List<string>(), false, "", "", new RateLimitRule("1s", 100, 3), 429)) .WithRateLimitOptions(new RateLimitOptions(true, "ClientId", () => new List<string>(), false, "", "", new RateLimitRule("1s", 100, 3), 429))
.WithUpstreamHttpMethod(new List<string> { "Get" }) .WithUpstreamHttpMethod(new List<string> { "Get" })
.WithUpstreamPathTemplate(upstreamTemplate) .WithUpstreamPathTemplate(upstreamTemplate)
.Build(); .Build();
var reRoute = new ReRouteBuilder() var reRoute = new ReRouteBuilder()
.WithDownstreamReRoute(downstreamReRoute) .WithDownstreamReRoute(downstreamReRoute)
@ -82,7 +82,7 @@ namespace Ocelot.UnitTests.RateLimit
.WithDownstreamReRoute(new DownstreamReRouteBuilder() .WithDownstreamReRoute(new DownstreamReRouteBuilder()
.WithEnableRateLimiting(true) .WithEnableRateLimiting(true)
.WithRateLimitOptions( .WithRateLimitOptions(
new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>() { "ocelotclient2" }, false, "", "", new RateLimitRule("1s", 100, 3), 429)) new Ocelot.Configuration.RateLimitOptions(true, "ClientId", () => new List<string>() { "ocelotclient2" }, false, "", "", new RateLimitRule("1s", 100, 3), 429))
.WithUpstreamHttpMethod(new List<string> { "Get" }) .WithUpstreamHttpMethod(new List<string> { "Get" })
.Build()) .Build())
.WithUpstreamHttpMethod(new List<string> { "Get" }) .WithUpstreamHttpMethod(new List<string> { "Get" })
@ -102,8 +102,8 @@ namespace Ocelot.UnitTests.RateLimit
private void WhenICallTheMiddlewareMultipleTime(int times) private void WhenICallTheMiddlewareMultipleTime(int times)
{ {
var clientId = "ocelotclient1"; var clientId = "ocelotclient1";
for (int i = 0; i < times; i++) for (int i = 0; i < times; i++)
{ {
var request = new HttpRequestMessage(new HttpMethod("GET"), _url); var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
@ -117,8 +117,8 @@ namespace Ocelot.UnitTests.RateLimit
private void WhenICallTheMiddlewareWithWhiteClient() private void WhenICallTheMiddlewareWithWhiteClient()
{ {
var clientId = "ocelotclient2"; var clientId = "ocelotclient2";
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
var request = new HttpRequestMessage(new HttpMethod("GET"), _url); var request = new HttpRequestMessage(new HttpMethod("GET"), _url);
@ -127,10 +127,10 @@ namespace Ocelot.UnitTests.RateLimit
_downstreamContext.HttpContext.Request.Headers.TryAdd("ClientId", clientId); _downstreamContext.HttpContext.Request.Headers.TryAdd("ClientId", clientId);
_middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult();
_responseStatusCode = (int)_downstreamContext.HttpContext.Response.StatusCode; _responseStatusCode = (int)_downstreamContext.HttpContext.Response.StatusCode;
} }
} }
private void ThenresponseStatusCodeIs429() private void ThenresponseStatusCodeIs429()
{ {
_responseStatusCode.ShouldBe(429); _responseStatusCode.ShouldBe(429);
@ -145,7 +145,7 @@ namespace Ocelot.UnitTests.RateLimit
internal class FakeStream : Stream internal class FakeStream : Stream
{ {
public override void Flush() public override void Flush()
{ {
//do nothing //do nothing
//throw new System.NotImplementedException(); //throw new System.NotImplementedException();
} }
@ -176,4 +176,4 @@ namespace Ocelot.UnitTests.RateLimit
public override long Length { get; } public override long Length { get; }
public override long Position { get; set; } public override long Position { get; set; }
} }
} }

View File

@ -38,6 +38,14 @@
error.ShouldBeOfType<RequestCanceledError>(); error.ShouldBeOfType<RequestCanceledError>();
} }
[Fact]
public void should_return_request_canceled_for_subtype()
{
var error = _mapper.Map(new SomeException());
error.ShouldBeOfType<RequestCanceledError>();
}
[Fact] [Fact]
public void should_return_error_from_mapper() public void should_return_error_from_mapper()
{ {
@ -56,5 +64,8 @@
error.ShouldBeOfType<AnyError>(); error.ShouldBeOfType<AnyError>();
} }
private class SomeException : OperationCanceledException
{ }
} }
} }

View File

@ -41,9 +41,10 @@ namespace Ocelot.UnitTests.Requester
public void should_call_services_correctly() public void should_call_services_correctly()
{ {
this.Given(x => x.GivenTheRequestIs()) this.Given(x => x.GivenTheRequestIs())
.And(x => x.GivenTheRequesterReturns(new OkResponse<HttpResponseMessage>(new HttpResponseMessage()))) .And(x => x.GivenTheRequesterReturns(new OkResponse<HttpResponseMessage>(new HttpResponseMessage(System.Net.HttpStatusCode.OK))))
.When(x => x.WhenICallTheMiddleware()) .When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenTheDownstreamResponseIsSet()) .Then(x => x.ThenTheDownstreamResponseIsSet())
.Then(x => InformationIsLogged())
.BDDfy(); .BDDfy();
} }
@ -57,6 +58,17 @@ namespace Ocelot.UnitTests.Requester
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_log_downstream_internal_server_error()
{
this.Given(x => x.GivenTheRequestIs())
.And(x => x.GivenTheRequesterReturns(
new OkResponse<HttpResponseMessage>(new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError))))
.When(x => x.WhenICallTheMiddleware())
.Then(x => x.WarningIsLogged())
.BDDfy();
}
private void ThenTheErrorIsSet() private void ThenTheErrorIsSet()
{ {
_downstreamContext.IsError.ShouldBeTrue(); _downstreamContext.IsError.ShouldBeTrue();
@ -98,5 +110,23 @@ namespace Ocelot.UnitTests.Requester
_downstreamContext.DownstreamResponse.Content.ShouldBe(_response.Data.Content); _downstreamContext.DownstreamResponse.Content.ShouldBe(_response.Data.Content);
_downstreamContext.DownstreamResponse.StatusCode.ShouldBe(_response.Data.StatusCode); _downstreamContext.DownstreamResponse.StatusCode.ShouldBe(_response.Data.StatusCode);
} }
private void WarningIsLogged()
{
_logger.Verify(
x => x.LogWarning(
It.IsAny<string>()
),
Times.Once);
}
private void InformationIsLogged()
{
_logger.Verify(
x => x.LogInformation(
It.IsAny<string>()
),
Times.Once);
}
} }
} }