diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index 7f9c0768..547d032f 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -1,40 +1,40 @@ -Rate Limiting -============= - -Thanks to `@catcherwong article `_ for inspiring me to finally write this documentation. - -Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much. - -OK so to get rate limiting working for a ReRoute you need to add the following json to it. - -.. code-block:: json - - "RateLimitOptions": { - "ClientWhitelist": [], - "EnableRateLimiting": true, - "Period": "1s", - "PeriodTimespan": 1, - "Limit": 1 - } - -ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting. -EnableRateLimiting - This value specifies enable endpoint rate limiting. -Period - This value specifies the period, such as 1s, 5m, 1h,1d and so on. -PeriodTimespan - This value specifies that we can retry after a certain number of seconds. -Limit - This value specifies the maximum number of requests that a client can make in a defined period. - -You can also set the following in the GlobalConfiguration part of ocelot.json - -.. code-block:: json - - "RateLimitOptions": { - "DisableRateLimitHeaders": false, - "QuotaExceededMessage": "Customize Tips!", - "HttpStatusCode": 999, - "ClientIdHeader" : "Test" - } - -DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Rety-After headers are disabled. -QuotaExceededMessage - This value specifies the exceeded message. -HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs. -ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId" +Rate Limiting +============= + +Thanks to `@catcherwong article `_ for inspiring me to finally write this documentation. + +Ocelot supports rate limiting of upstream requests so that your downstream services do not become overloaded. This feature was added by @geffzhang on GitHub! Thanks very much. + +OK so to get rate limiting working for a ReRoute you need to add the following json to it. + +.. code-block:: json + + "RateLimitOptions": { + "ClientWhitelist": [], + "EnableRateLimiting": true, + "Period": "1s", + "PeriodTimespan": 1, + "Limit": 1 + } + +ClientWhitelist - This is an array that contains the whitelist of the client. It means that the client in this array will not be affected by the rate limiting. +EnableRateLimiting - This value specifies enable endpoint rate limiting. +Period - This value specifies the period that the limit applies to, such as 1s, 5m, 1h,1d and so on. If you make more requests in the period than the limit allows then you need to wait for PeriodTimespan to elapse before you make another request. +PeriodTimespan - This value specifies that we can retry after a certain number of seconds. +Limit - This value specifies the maximum number of requests that a client can make in a defined period. + +You can also set the following in the GlobalConfiguration part of ocelot.json + +.. code-block:: json + + "RateLimitOptions": { + "DisableRateLimitHeaders": false, + "QuotaExceededMessage": "Customize Tips!", + "HttpStatusCode": 999, + "ClientIdHeader" : "Test" + } + +DisableRateLimitHeaders - This value specifies whether X-Rate-Limit and Rety-After headers are disabled. +QuotaExceededMessage - This value specifies the exceeded message. +HttpStatusCode - This value specifies the returned HTTP Status code when rate limiting occurs. +ClientIdHeader - Allows you to specifiy the header that should be used to identify clients. By default it is "ClientId" diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs index 82124c85..4b0d5165 100644 --- a/src/Ocelot/RateLimit/RateLimitCore.cs +++ b/src/Ocelot/RateLimit/RateLimitCore.cs @@ -1,148 +1,148 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; - -namespace Ocelot.RateLimit -{ - public class RateLimitCore - { - private readonly IRateLimitCounterHandler _counterHandler; - private static readonly object _processLocker = new object(); - - public RateLimitCore(IRateLimitCounterHandler counterStore) - { - _counterHandler = counterStore; - } - - public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1); - var rule = option.RateLimitRule; - - var counterId = ComputeCounterKey(requestIdentity, option); - - // serial reads and writes - lock (_processLocker) - { - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - // entry has not expired - if (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period) >= DateTime.UtcNow) - { - // increment request count - var totalRequests = entry.Value.TotalRequests + 1; - - // deep copy - counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); - } - } - } - - if (counter.TotalRequests > rule.Limit) - { - var retryAfter = RetryAfterFrom(counter.Timestamp, rule); - if (retryAfter > 0) - { - var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan); - _counterHandler.Set(counterId, counter, expirationTime); - } - else - { - _counterHandler.Remove(counterId); - } - } - else - { - var expirationTime = ConvertToTimeSpan(rule.Period); - _counterHandler.Set(counterId, counter, expirationTime); - } - - return counter; - } - - public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime) - { - var counterId = ComputeCounterKey(requestIdentity, option); - var rule = option.RateLimitRule; - - // stores: id (string) - timestamp (datetime) - total_requests (long) - _counterHandler.Set(counterId, counter, expirationTime); - } - - public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var rule = option.RateLimitRule; - RateLimitHeaders headers = null; - var counterId = ComputeCounterKey(requestIdentity, option); - var entry = _counterHandler.Get(counterId); - if (entry.HasValue) - { - headers = new RateLimitHeaders(context, rule.Period, - (rule.Limit - entry.Value.TotalRequests).ToString(), - (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo) - ); - } - else - { - headers = new RateLimitHeaders(context, - rule.Period, - rule.Limit.ToString(), - (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); - } - - return headers; - } - - public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option) - { - var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}"; - - var idBytes = Encoding.UTF8.GetBytes(key); - - byte[] hashBytes; - - using (var algorithm = SHA1.Create()) - { - hashBytes = algorithm.ComputeHash(idBytes); - } - - return BitConverter.ToString(hashBytes).Replace("-", string.Empty); - } - - public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) - { - var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); - var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds); - retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; - return retryAfter; - } - - public TimeSpan ConvertToTimeSpan(string timeSpan) - { - var l = timeSpan.Length - 1; - var value = timeSpan.Substring(0, l); - var type = timeSpan.Substring(l, 1); - - switch (type) - { - case "d": - return TimeSpan.FromDays(double.Parse(value)); - case "h": - return TimeSpan.FromHours(double.Parse(value)); - case "m": - return TimeSpan.FromMinutes(double.Parse(value)); - case "s": - return TimeSpan.FromSeconds(double.Parse(value)); - default: - throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}"); - } - } - } -} +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + public class RateLimitCore + { + private readonly IRateLimitCounterHandler _counterHandler; + private static readonly object _processLocker = new object(); + + public RateLimitCore(IRateLimitCounterHandler counterStore) + { + _counterHandler = counterStore; + } + + public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1); + var rule = option.RateLimitRule; + + var counterId = ComputeCounterKey(requestIdentity, option); + + // serial reads and writes + lock (_processLocker) + { + var entry = _counterHandler.Get(counterId); + if (entry.HasValue) + { + // entry has not expired + if (entry.Value.Timestamp + TimeSpan.FromSeconds(rule.PeriodTimespan) >= DateTime.UtcNow) + { + // increment request count + var totalRequests = entry.Value.TotalRequests + 1; + + // deep copy + counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); + } + } + } + + if (counter.TotalRequests > rule.Limit) + { + var retryAfter = RetryAfterFrom(counter.Timestamp, rule); + if (retryAfter > 0) + { + var expirationTime = TimeSpan.FromSeconds(rule.PeriodTimespan); + _counterHandler.Set(counterId, counter, expirationTime); + } + else + { + _counterHandler.Remove(counterId); + } + } + else + { + var expirationTime = ConvertToTimeSpan(rule.Period); + _counterHandler.Set(counterId, counter, expirationTime); + } + + return counter; + } + + public void SaveRateLimitCounter(ClientRequestIdentity requestIdentity, RateLimitOptions option, RateLimitCounter counter, TimeSpan expirationTime) + { + var counterId = ComputeCounterKey(requestIdentity, option); + var rule = option.RateLimitRule; + + // stores: id (string) - timestamp (datetime) - total_requests (long) + _counterHandler.Set(counterId, counter, expirationTime); + } + + public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + var rule = option.RateLimitRule; + RateLimitHeaders headers = null; + var counterId = ComputeCounterKey(requestIdentity, option); + var entry = _counterHandler.Get(counterId); + if (entry.HasValue) + { + headers = new RateLimitHeaders(context, rule.Period, + (rule.Limit - entry.Value.TotalRequests).ToString(), + (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo) + ); + } + else + { + headers = new RateLimitHeaders(context, + rule.Period, + rule.Limit.ToString(), + (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + } + + return headers; + } + + public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}"; + + var idBytes = Encoding.UTF8.GetBytes(key); + + byte[] hashBytes; + + using (var algorithm = SHA1.Create()) + { + hashBytes = algorithm.ComputeHash(idBytes); + } + + return BitConverter.ToString(hashBytes).Replace("-", string.Empty); + } + + public int RetryAfterFrom(DateTime timestamp, RateLimitRule rule) + { + var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); + var retryAfter = Convert.ToInt32(TimeSpan.FromSeconds(rule.PeriodTimespan).TotalSeconds); + retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; + return retryAfter; + } + + public TimeSpan ConvertToTimeSpan(string timeSpan) + { + var l = timeSpan.Length - 1; + var value = timeSpan.Substring(0, l); + var type = timeSpan.Substring(l, 1); + + switch (type) + { + case "d": + return TimeSpan.FromDays(double.Parse(value)); + case "h": + return TimeSpan.FromHours(double.Parse(value)); + case "m": + return TimeSpan.FromMinutes(double.Parse(value)); + case "s": + return TimeSpan.FromSeconds(double.Parse(value)); + default: + throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}"); + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs index dc9d5eef..6963f20c 100644 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -1,171 +1,236 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using Shouldly; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.AcceptanceTests -{ - public class ClientRateLimitTests : IDisposable - { - private IWebHost _builder; - private readonly Steps _steps; - private int _counterOne; - - public ClientRateLimitTests() - { - _steps = new Steps(); - } - - public void Dispose() - { - _builder?.Dispose(); - _steps.Dispose(); - } - - [Fact] - public void should_call_withratelimiting() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule() - { - EnableRateLimiting = true, - ClientWhitelist = new List(), - Limit = 3, - Period = "1s", - PeriodTimespan = 1000 - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - RateLimitOptions = new FileRateLimitOptions() - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = "", - RateLimitCounterPrefix = "", - HttpStatusCode = 428 - }, - RequestIdKey ="oceclientrequest" - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) - .BDDfy(); - } - - [Fact] - public void should_call_middleware_withWhitelistClient() - { - var configuration = new FileConfiguration - { - ReRoutes = new List - { - new FileReRoute - { - DownstreamPathTemplate = "/api/ClientRateLimit", - DownstreamHostAndPorts = new List - { - new FileHostAndPort - { - Host = "localhost", - Port = 51876, - } - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/api/ClientRateLimit", - UpstreamHttpMethod = new List { "Get" }, - RequestIdKey = _steps.RequestIdKey, - - RateLimitOptions = new FileRateLimitRule() - { - EnableRateLimiting = true, - ClientWhitelist = new List() { "ocelotclient1"}, - Limit = 3, - Period = "1s", - PeriodTimespan = 100 - } - } - }, - GlobalConfiguration = new FileGlobalConfiguration() - { - RateLimitOptions = new FileRateLimitOptions() - { - ClientIdHeader = "ClientId", - DisableRateLimitHeaders = false, - QuotaExceededMessage = "", - RateLimitCounterPrefix = "" - }, - RequestIdKey = "oceclientrequest" - } - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", "/api/ClientRateLimit")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) - .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) - .BDDfy(); - } - - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) - { - _builder = new WebHostBuilder() - .UseUrls(baseUrl) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseUrls(baseUrl) - .Configure(app => - { - app.UsePathBase(basePath); - app.Run(context => - { - _counterOne++; - context.Response.StatusCode = 200; - context.Response.WriteAsync(_counterOne.ToString()); - return Task.CompletedTask; - }); - }) - .Build(); - - _builder.Start(); - } - } -} +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ClientRateLimitTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private int _counterOne; + + public ClientRateLimitTests() + { + _steps = new Steps(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + + [Fact] + public void should_call_withratelimiting() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51876, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 1000 + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + RateLimitOptions = new FileRateLimitOptions() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "", + RateLimitCounterPrefix = "", + HttpStatusCode = 428 + }, + RequestIdKey ="oceclientrequest" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", "/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .BDDfy(); + } + + [Fact] + public void should_wait_for_period_timespan_to_elapse_before_making_next_request() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51926, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 2 + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + RateLimitOptions = new FileRateLimitOptions() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "", + RateLimitCounterPrefix = "", + HttpStatusCode = 428 + }, + RequestIdKey ="oceclientrequest" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51926", "/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .And(x => _steps.GivenIWait(1000)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .And(x => _steps.GivenIWait(1000)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .BDDfy(); + } + + [Fact] + public void should_call_middleware_withWhitelistClient() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51876, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List() { "ocelotclient1"}, + Limit = 3, + Period = "1s", + PeriodTimespan = 100 + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + RateLimitOptions = new FileRateLimitOptions() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "", + RateLimitCounterPrefix = "" + }, + RequestIdKey = "oceclientrequest" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", "/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(baseUrl) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(context => + { + _counterOne++; + context.Response.StatusCode = 200; + context.Response.WriteAsync(_counterOne.ToString()); + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index f3c1a6d2..67723ccb 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -183,6 +183,11 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + internal void GivenIWait(int wait) + { + Thread.Sleep(wait); + } + public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) { _webHostBuilder = new WebHostBuilder();