Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ const config: UserConfig<DefaultTheme.Config> = {
{text: 'Fluent Validation', link: '/guide/http/fluentvalidation'},
{text: 'Problem Details', link: '/guide/http/problemdetails'},
{text: 'Caching', link: '/guide/http/caching'},
{text: 'Rate Limiting', link: '/guide/http/rate-limiting'},
{text: 'Streaming and SSE', link: '/guide/http/streaming'},
{text: 'HTTP Messaging Transport', link: '/guide/http/transport'},
{text: 'Integration Testing with Alba', link: '/guide/http/integration-testing'}
Expand Down
112 changes: 112 additions & 0 deletions docs/guide/http/rate-limiting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Rate Limiting

Wolverine.HTTP endpoints are standard ASP.NET Core endpoints, so ASP.NET Core's [built-in rate limiting middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) works directly with no additional Wolverine-specific setup required.

## Setup

First, register the rate limiting services and define one or more rate limiting policies in your application's service configuration:

<!-- snippet: sample_rate_limiting_configuration -->
<a id='snippet-sample_rate_limiting_configuration'></a>
```cs
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 1;
opt.Window = TimeSpan.FromSeconds(10);
opt.QueueLimit = 0;
});
options.RejectionStatusCode = 429;
});
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L72-L84' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_rate_limiting_configuration' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Then add the rate limiting middleware to the request pipeline. This must be placed **before** `MapWolverineEndpoints()`:

<!-- snippet: sample_use_rate_limiter_middleware -->
<a id='snippet-sample_use_rate_limiter_middleware'></a>
```cs
app.UseRateLimiter();
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/Program.cs#L199-L201' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_use_rate_limiter_middleware' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Per-Endpoint Rate Limiting

Apply the `[EnableRateLimiting]` attribute to individual Wolverine.HTTP endpoints to enforce a named rate limiting policy:

<!-- snippet: sample_rate_limited_endpoint -->
<a id='snippet-sample_rate_limited_endpoint'></a>
```cs
[WolverineGet("/api/rate-limited")]
[EnableRateLimiting("fixed")]
public static string GetRateLimited()
{
return "OK";
}
```
<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Http/WolverineWebApi/RateLimiting/RateLimitedEndpoints.cs#L9-L16' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_rate_limited_endpoint' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

When the rate limit is exceeded, the middleware returns a `429 Too Many Requests` response automatically.

## Disabling Rate Limiting

If you have a global rate limiting policy applied, you can opt specific endpoints out using the `[DisableRateLimiting]` attribute:

```cs
[WolverineGet("/api/health")]
[DisableRateLimiting]
public static string HealthCheck()
{
return "Healthy";
}
```

## Global Rate Limiting

You can apply a global rate limiting policy that applies to all endpoints by using a global limiter instead of (or in addition to) named policies:

```cs
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
}));
options.RejectionStatusCode = 429;
});
```

## Applying Rate Limiting via ConfigureEndpoints

You can use Wolverine's `ConfigureEndpoints()` to apply rate limiting metadata to all Wolverine.HTTP endpoints programmatically:

```cs
app.MapWolverineEndpoints(opts =>
{
opts.ConfigureEndpoints(httpChain =>
{
// Apply rate limiting to all Wolverine endpoints
httpChain.WithMetadata(new EnableRateLimitingAttribute("fixed"));
});
});
```

## Available Rate Limiting Algorithms

ASP.NET Core provides several built-in rate limiting algorithms:

- **Fixed window** -- limits requests in fixed time intervals
- **Sliding window** -- uses a sliding time window for smoother rate limiting
- **Token bucket** -- allows bursts of traffic up to a configured limit
- **Concurrency** -- limits the number of concurrent requests

See the [ASP.NET Core rate limiting documentation](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit) for full details on each algorithm and advanced configuration options.
42 changes: 42 additions & 0 deletions src/Http/Wolverine.Http.Tests/RateLimiting/RateLimitingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Alba;
using Shouldly;

namespace Wolverine.Http.Tests.RateLimiting;

public class RateLimitingTests : IntegrationContext
{
public RateLimitingTests(AppFixture fixture) : base(fixture)
{
}

[Fact]
public async Task rate_limited_endpoint_returns_429_when_limit_exceeded()
{
// First request should succeed
await Host.Scenario(s =>
{
s.Get.Url("/api/rate-limited");
s.StatusCodeShouldBeOk();
});

// Second request within the window should be rate limited
await Host.Scenario(s =>
{
s.Get.Url("/api/rate-limited");
s.StatusCodeShouldBe(429);
});
}

[Fact]
public async Task non_rate_limited_endpoint_always_succeeds()
{
for (int i = 0; i < 5; i++)
{
await Host.Scenario(s =>
{
s.Get.Url("/api/not-rate-limited");
s.StatusCodeShouldBeOk();
});
}
}
}
19 changes: 19 additions & 0 deletions src/Http/WolverineWebApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Threading.RateLimiting;
using IntegrationTests;
using JasperFx;
using JasperFx.CodeGeneration;
Expand All @@ -9,6 +10,7 @@
using Marten;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.EntityFrameworkCore;
using Wolverine;
using Wolverine.AdminApi;
Expand Down Expand Up @@ -67,6 +69,19 @@ public static async Task<int> Main(string[] args)

builder.Services.AddAuthorization();

#region sample_rate_limiting_configuration
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.PermitLimit = 1;
opt.Window = TimeSpan.FromSeconds(10);
opt.QueueLimit = 0;
});
options.RejectionStatusCode = 429;
});
#endregion

builder.Services.AddDbContextWithWolverineIntegration<ItemsDbContext>(x =>
x.UseNpgsql(Servers.PostgresConnectionString));

Expand Down Expand Up @@ -191,6 +206,10 @@ public static async Task<int> Main(string[] args)

app.UseAuthorization();

#region sample_use_rate_limiter_middleware
app.UseRateLimiter();
#endregion

// These routes are for doing
OpenApiEndpoints.BuildComparisonRoutes(app);

Expand Down
22 changes: 22 additions & 0 deletions src/Http/WolverineWebApi/RateLimiting/RateLimitedEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Microsoft.AspNetCore.RateLimiting;
using Wolverine.Http;

namespace WolverineWebApi.RateLimiting;

public static class RateLimitedEndpoints
{
#region sample_rate_limited_endpoint
[WolverineGet("/api/rate-limited")]
[EnableRateLimiting("fixed")]
public static string GetRateLimited()
{
return "OK";
}
#endregion

[WolverineGet("/api/not-rate-limited")]
public static string GetNotRateLimited()
{
return "OK";
}
}
Loading