Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions samples/Ocelot.Samples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabri
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Web", "Web\Ocelot.Samples.Web.csproj", "{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.RateLimiter", "RateLimiter\Ocelot.Samples.RateLimiter.csproj", "{C4B2D4B9-D568-42DA-A203-6C33BA2E055D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -93,6 +95,10 @@ Global
{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.Build.0 = Release|Any CPU
{C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
13 changes: 13 additions & 0 deletions samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Ocelot\Ocelot.csproj" />
</ItemGroup>

</Project>
11 changes: 11 additions & 0 deletions samples/RateLimiter/Ocelot.Samples.RateLimiter.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@RateLimiterSample_HostAddress = http://localhost:5202

GET {{RateLimiterSample_HostAddress}}/laura
Accept: application/json

###

GET {{RateLimiterSample_HostAddress}}/tom
Accept: application/json

###
26 changes: 26 additions & 0 deletions samples/RateLimiter/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Microsoft.AspNetCore.RateLimiting;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using System.Threading.RateLimiting;

var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("ocelot.json");
builder.Services.AddOcelot();

builder.Services.AddRateLimiter(op =>
{
op.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 2;
options.Window = TimeSpan.FromSeconds(12);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 0;
});
});
Comment on lines +10 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree! In this case, we haven't proposed anything new.
I think we should provide the user with an option to choose between configuration approaches. We could consider having a separate Ocelot rate-limiting rule to manage app auto-configurations.

Copy link
Collaborator

@ggnaegi ggnaegi Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raman-m it's basically what i mentioned:
Yes, that's the main concern; we essentially need to reinvent the wheel for the middleware part. I've tried to "dynamize" the policies myself, but I believe it’s not possible.
The policies are setup during startup, you can't modify them after that, and you can't mix them (as of 2024, maybe now it's possible).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't mix policies, but we can define as many as needed, right?
From your words, did you mean that the "fixed" policy is global in this code?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's global and they can't be modified.


var app = builder.Build();
app.UseHttpsRedirection();

await app.UseOcelot();

app.Run();
41 changes: 41 additions & 0 deletions samples/RateLimiter/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:12083",
"sslPort": 44358
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "http://localhost:5202",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:7116;http://localhost:5202",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "weatherforecast",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
8 changes: 8 additions & 0 deletions samples/RateLimiter/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions samples/RateLimiter/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
46 changes: 46 additions & 0 deletions samples/RateLimiter/ocelot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"Routes": [
{
"UpstreamHttpMethod": [ "Get" ],
"UpstreamPathTemplate": "/laura",
"DownstreamPathTemplate": "/fact",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{ "Host": "catfact.ninja", "Port": 443 }
],
"Key": "Laura",
"RateLimitOptions": {
"EnableRateLimiting": true,
"Period": "5s",
"PeriodTimespan": 1,
"Limit": 1
}
},
{
"UpstreamHttpMethod": [ "Get" ],
"UpstreamPathTemplate": "/tom",
"DownstreamPathTemplate": "/fact",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{ "Host": "catfact.ninja", "Port": 443 }
],
"Key": "Tom",
"RateLimitOptions": {
"EnableRateLimiting": true,
"Policy": "fixed"
}
}
],
"Aggregates": [
{
"UpstreamPathTemplate": "/",
"RouteKeys": [ "Tom", "Laura" ]
}
],
"GlobalConfiguration": {
"RateLimitOptions": {
"QuotaExceededMessage": "Customize Tips!",
"HttpStatusCode": 418 // I'm a teapot
}
}
}
9 changes: 8 additions & 1 deletion src/Ocelot/Configuration/Builder/RateLimitOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class RateLimitOptionsBuilder
private string _rateLimitCounterPrefix;
private RateLimitRule _rateLimitRule;
private int _httpStatusCode;
private string _rateLimitPolicyName;

public RateLimitOptionsBuilder WithEnableRateLimiting(bool enableRateLimiting)
{
Expand Down Expand Up @@ -59,11 +60,17 @@ public RateLimitOptionsBuilder WithHttpStatusCode(int httpStatusCode)
return this;
}

public RateLimitOptionsBuilder WithRateLimitPolicyName(string policyName)
{
_rateLimitPolicyName = policyName;
return this;
}

public RateLimitOptions Build()
{
return new RateLimitOptions(_enableRateLimiting, _clientIdHeader, _getClientWhitelist,
_disableRateLimitHeaders, _quotaExceededMessage, _rateLimitCounterPrefix,
_rateLimitRule, _httpStatusCode);
_rateLimitRule, _httpStatusCode, _rateLimitPolicyName);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public RateLimitOptions Create(FileRateLimitRule fileRateLimitRule, FileGlobalCo
.WithRateLimitRule(new RateLimitRule(fileRateLimitRule.Period,
fileRateLimitRule.PeriodTimespan,
fileRateLimitRule.Limit))
.WithRateLimitPolicyName(fileRateLimitRule.Policy)
.Build();
}

Expand Down
24 changes: 21 additions & 3 deletions src/Ocelot/Configuration/File/FileRateLimitRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public FileRateLimitRule(FileRateLimitRule from)
Limit = from.Limit;
Period = from.Period;
PeriodTimespan = from.PeriodTimespan;
Policy = from.Policy;
}

/// <summary>
Expand Down Expand Up @@ -56,6 +57,14 @@ public FileRateLimitRule(FileRateLimitRule from)
/// </value>
public long Limit { get; set; }

/// <summary>
/// Rate limit policy name. It only takes effect if rate limit middleware type is set to DotNet.
/// </summary>
/// <value>
/// A string of rate limit policy name.
/// </value>
public string Policy { get; set; }

/// <inheritdoc/>
public override string ToString()
{
Expand All @@ -65,11 +74,20 @@ public override string ToString()
}

var sb = new StringBuilder();
sb.Append(

if (!string.IsNullOrWhiteSpace(Policy))
{
sb.Append($"{nameof(Policy)}:{Policy}");
}
else
{
sb.Append(
$"{nameof(Period)}:{Period},{nameof(PeriodTimespan)}:{PeriodTimespan:F},{nameof(Limit)}:{Limit},{nameof(ClientWhitelist)}:[");

sb.AppendJoin(',', ClientWhitelist);
sb.Append(']');
sb.AppendJoin(',', ClientWhitelist);
sb.Append(']');
}

return sb.ToString();
}
}
Expand Down
47 changes: 25 additions & 22 deletions src/Ocelot/Configuration/RateLimitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class RateLimitOptions
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, string rateLimitPolicy = null)
{
EnableRateLimiting = enableRateLimiting;
ClientIdHeader = clientIdHeader;
Expand All @@ -18,73 +18,76 @@ public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, Func<Lis
RateLimitCounterPrefix = rateLimitCounterPrefix;
RateLimitRule = rateLimitRule;
HttpStatusCode = httpStatusCode;
}

/// <summary>
/// Gets a Rate Limit rule.
Policy = rateLimitPolicy;
}

/// <summary>
/// Gets a Rate Limit rule.
/// </summary>
/// <value>
/// A <see cref="Configuration.RateLimitRule"/> object that represents the rule.
/// </value>
public RateLimitRule RateLimitRule { get; }

public RateLimitRule RateLimitRule { get; }
/// <summary>
/// Gets the list of white listed clients.
/// </summary>
/// <value>
/// A <see cref="List{T}"/> (where T is <see cref="string"/>) collection with white listed clients.
/// </value>
public List<string> ClientWhitelist => _getClientWhitelist();

public List<string> ClientWhitelist => _getClientWhitelist();
/// <summary>
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId.
/// </summary>
/// <value>
/// A string value with the HTTP header.
/// </value>
public string ClientIdHeader { get; }

public string ClientIdHeader { get; }
/// <summary>
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests).
/// </summary>
/// <value>
/// An integer value with the HTTP Status code.
/// <para>Default value: 429 (Too Many Requests).</para>
/// An integer value with the HTTP Status code.
/// <para>Default value: 429 (Too Many Requests).</para>
/// </value>
public int HttpStatusCode { get; }

public int HttpStatusCode { get; }
/// <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.
/// <para>If none specified the default will be: "API calls quota exceeded! maximum admitted {0} per {1}".</para>
/// </summary>
/// <value>
/// A string value with a formatter for the QuotaExceeded response message.
/// <para>Default will be: "API calls quota exceeded! maximum admitted {0} per {1}".</para>
/// </value>
public string QuotaExceededMessage { get; }

public string QuotaExceededMessage { get; }
/// <summary>
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key.
/// </summary>
/// <value>
/// A string value with the counter prefix.
/// </value>
public string RateLimitCounterPrefix { get; }

public string RateLimitCounterPrefix { get; }
/// <summary>
/// Enables endpoint rate limiting based URL path and HTTP verb.
/// </summary>
/// <value>
/// A boolean value for enabling endpoint rate limiting based URL path and HTTP verb.
/// </value>
public bool EnableRateLimiting { get; }

public bool EnableRateLimiting { get; }
/// <summary>
/// Disables <c>X-Rate-Limit</c> and <c>Retry-After</c> headers.
/// </summary>
/// <value>
/// A boolean value for disabling <c>X-Rate-Limit</c> and <c>Retry-After</c> headers.
/// </value>
public bool DisableRateLimitHeaders { get; }

public string Policy { get; }
}
}
Loading