-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Implement minimal RateLimitingMiddleware #41008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
2721e4f
caa47a6
d649fca
6825683
f7e82a5
c5f2d94
7782861
2c683fc
301a4ee
8b1e074
7870d62
16bdcf3
4f20092
a417766
0c43a25
017797d
2b406b0
75aa2cf
ade42e0
d025eb8
b7affd6
ddddbd4
9abcb07
047ab3d
cea2af6
4761f1e
ac0ad26
fc92f12
43b16a6
9d3dffb
385d939
0b9875b
9d992ca
b90f534
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<Description>ASP.NET Core middleware for enforcing rate limiting in an application</Description> | ||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> | ||
<GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
<PackageTags>aspnetcore</PackageTags> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> | ||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" /> | ||
<Reference Include="Microsoft.Extensions.Options" /> | ||
<Reference Include="System.Threading.RateLimiting" /> | ||
|
||
<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" /> | ||
</ItemGroup> | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#nullable enable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.RateLimiting.RateLimitingOptions!>! options) -> void | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter<HttpContext>(System.Threading.RateLimiting.PartitionedRateLimiter<Microsoft.AspNetCore.Http.HttpContext!>! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter<Microsoft.AspNetCore.Http.HttpContext!>? | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate! | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
/// <summary> | ||
/// Limits the rate of requests allowed in the application, based on limits set by a user-provided <see cref="PartitionedRateLimiter{TResource}"/>. | ||
/// </summary> | ||
public partial class RateLimitingMiddleware | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
|
||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private readonly RequestDelegate _next; | ||
private readonly RequestDelegate _onRejected; | ||
private readonly ILogger _logger; | ||
private readonly PartitionedRateLimiter<HttpContext> _limiter; | ||
private RateLimitLease? _lease; | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// Creates a new <see cref="RateLimitingMiddleware"/>. | ||
/// </summary> | ||
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param> | ||
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> used for logging.</param> | ||
/// <param name="options">The options for the middleware.</param> | ||
public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions<RateLimitingOptions> options) | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
_next = next ?? throw new ArgumentNullException(nameof(next)); | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (options.Value.Limiter == null) | ||
{ | ||
throw new ArgumentException("The value of 'options.Limiter' must not be null.", nameof(options)); | ||
} | ||
|
||
if (options.Value.OnRejected == null) | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
throw new ArgumentException("The value of 'options.OnRejected' must not be null.", nameof(options)); | ||
} | ||
|
||
if (loggerFactory == null) | ||
{ | ||
throw new ArgumentNullException(nameof(loggerFactory)); | ||
} | ||
|
||
_logger = loggerFactory.CreateLogger<RateLimitingMiddleware>(); | ||
_limiter = options.Value.Limiter; | ||
_onRejected = options.Value.OnRejected; | ||
} | ||
|
||
// TODO - EventSource? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be valuable to add EventSource logging to this, part of the next pass? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with waiting for the next pass. It might be better to add EventSource logging to the rate limiter implemenations, but that might be too low level to be useful if there are many. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an issue tracking this yet? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
/// <summary> | ||
/// Invokes the logic of the middleware. | ||
/// </summary> | ||
/// <param name="context">The <see cref="HttpContext"/>.</param> | ||
/// <returns>A <see cref="Task"/> that completes when the request leaves.</returns> | ||
public async Task Invoke(HttpContext context) | ||
{ | ||
var acquireLeaseTask = TryAcquireAsync(context); | ||
|
||
// Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets. | ||
bool result; | ||
|
||
if (acquireLeaseTask.IsCompleted) | ||
{ | ||
result = acquireLeaseTask.Result; | ||
} | ||
else | ||
{ | ||
result = await acquireLeaseTask; | ||
} | ||
|
||
if (result) | ||
{ | ||
try | ||
{ | ||
await _next(context); | ||
} | ||
finally | ||
{ | ||
OnCompletion(); | ||
} | ||
} | ||
else | ||
{ | ||
RateLimiterLog.RequestRejectedLimitsExceeded(_logger); | ||
halter73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await _onRejected(context); | ||
} | ||
} | ||
|
||
private ValueTask<bool> TryAcquireAsync(HttpContext context) | ||
{ | ||
// a return value of 'false' indicates that the request is rejected | ||
// a return value of 'true' indicates that the request may proceed | ||
|
||
var lease = _limiter.Acquire(context); | ||
if (lease.IsAcquired) | ||
{ | ||
_lease = lease; | ||
return ValueTask.FromResult(true); | ||
} | ||
|
||
var task = _limiter.WaitAsync(context); | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (task.IsCompletedSuccessfully) | ||
{ | ||
lease = task.Result; | ||
if (lease.IsAcquired) | ||
{ | ||
_lease = lease; | ||
return ValueTask.FromResult(true); | ||
} | ||
|
||
return ValueTask.FromResult(false); | ||
} | ||
|
||
return Awaited(task); | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
private void OnCompletion() | ||
{ | ||
if (_lease != null) | ||
{ | ||
_lease.Dispose(); | ||
} | ||
} | ||
|
||
private async ValueTask<bool> Awaited(ValueTask<RateLimitLease> task) | ||
{ | ||
var lease = await task; | ||
|
||
if (lease.IsAcquired) | ||
{ | ||
_lease = lease; | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
private static partial class RateLimiterLog | ||
{ | ||
[LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request with a '503 server not available' error", EventName = "RequestRejectedLimitsExceeded")] | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
internal static partial void RequestRejectedLimitsExceeded(ILogger logger); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
/// <summary> | ||
/// Specifies options for the <see cref="RateLimitingMiddleware"/>. | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// </summary> | ||
public class RateLimitingOptions | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
// TODO - Provide a default? | ||
private PartitionedRateLimiter<HttpContext>? _limiter; | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// Gets the <see cref="PartitionedRateLimiter{TResource}"/> | ||
/// </summary> | ||
public PartitionedRateLimiter<HttpContext>? Limiter | ||
{ | ||
get => _limiter; | ||
} | ||
|
||
/// <summary> | ||
/// Adds a new rate limiter. | ||
/// </summary> | ||
/// <param name="limiter">The <see cref="PartitionedRateLimiter{TResource}"/> to be added.</param> | ||
public RateLimitingOptions AddLimiter<HttpContext>(PartitionedRateLimiter<Http.HttpContext> limiter) | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (limiter == null) | ||
{ | ||
throw new ArgumentNullException(nameof(limiter)); | ||
} | ||
_limiter = limiter; | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// A <see cref="RequestDelegate"/> that handles requests rejected by this middleware. | ||
/// If it doesn't modify the response, an empty 503 response will be written. | ||
/// </summary> | ||
public RequestDelegate OnRejected { get; set; } = context => | ||
{ | ||
return Task.CompletedTask; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Http" /> | ||
<Reference Include="Microsoft.AspNetCore.RateLimiting" /> | ||
</ItemGroup> | ||
</Project> |
Uh oh!
There was an error while loading. Please reload this page.