Skip to content

Commit 98e9d3a

Browse files
committed
Centralize rate limit response constants
1 parent 11477f4 commit 98e9d3a

13 files changed

Lines changed: 109 additions & 56 deletions

ManagedCode.Orleans.RateLimiting.Client/Extensions/SignalRServerBuilderExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ public static ISignalRServerBuilder AddOrleansRateLimiting(
1515
string configurationName,
1616
RateLimitPartitionKind partitionKind = RateLimitPartitionKind.User)
1717
{
18-
builder.Services.AddOrleansRateLimiting(options => options.AddToPolicy("SignalR", partitionKind, configurationName, required: true));
18+
builder.Services.AddOrleansRateLimiting(options =>
19+
options.AddToPolicy(SignalRRateLimitingDefaults.PolicyName, partitionKind, configurationName, required: true));
1920
builder.Services.Configure<SignalRRateLimitingOptions>(options =>
2021
{
21-
options.PolicyName = "SignalR";
22+
options.PolicyName = SignalRRateLimitingDefaults.PolicyName;
2223
options.ConfigurationName = configurationName;
2324
options.PartitionKind = partitionKind;
2425
});

ManagedCode.Orleans.RateLimiting.Client/Middlewares/OrleansBaseRateLimitingMiddleware.cs

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Net;
43
using System.Reflection;
54
using System.Threading.Tasks;
65
using ManagedCode.Orleans.RateLimiting.Client.Attributes;
@@ -38,26 +37,14 @@ public async Task Invoke(HttpContext httpContext)
3837

3938
AddLimiters(httpContext, holder);
4039

41-
// throw too many requests if any of the limiters is null code 429
4240
var error = await holder.AcquireAsync();
4341
if (error is null)
4442
{
4543
await _next(httpContext);
4644
}
4745
else
4846
{
49-
if (httpContext.Response.HasStarted)
50-
return;
51-
52-
httpContext.Response.Clear();
53-
httpContext.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
54-
await httpContext.Response.WriteAsJsonAsync(new
55-
{
56-
StatusCode = (int)HttpStatusCode.TooManyRequests,
57-
Error = "Too many requests",
58-
error.Reason,
59-
RetryAfter = error.RetryAfter.ToString()
60-
});
47+
await RateLimitResponseWriter.WriteTooManyRequestsAsync(httpContext, error);
6148
}
6249
}
6350

@@ -95,13 +82,13 @@ protected static (T attribute, string? postfix)? TryGetAttribute<T>(HttpContext
9582
var limiter = _client.GetRateLimiterByConfig(key, configurationName, _services.GetServices<RateLimiterConfig>());
9683

9784
if (limiter is null)
98-
_logger.LogError("Configuration {ConfigurationName} not found for RateLimiter", configurationName);
85+
_logger.LogError(RateLimitMiddlewareConstants.ConfigurationNotFoundLogMessage, configurationName);
9986

10087
return limiter;
10188
}
10289

10390
protected static string CreateKey(params string[] parts)
10491
{
105-
return string.Join(":", parts);
92+
return string.Join(RateLimitMiddlewareConstants.KeySeparator, parts);
10693
}
10794
}

ManagedCode.Orleans.RateLimiting.Client/Middlewares/OrleansRequestRateLimitingMiddleware.cs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System.Collections.Generic;
22
using System.Linq;
3-
using System.Net;
43
using System.Security.Claims;
54
using System.Threading.Tasks;
65
using ManagedCode.Orleans.RateLimiting.Client.Extensions;
@@ -39,39 +38,31 @@ public async Task Invoke(HttpContext httpContext)
3938
return;
4039
}
4140

42-
if (httpContext.Response.HasStarted)
43-
return;
44-
45-
httpContext.Response.Clear();
46-
httpContext.Response.StatusCode = (int)HttpStatusCode.TooManyRequests;
47-
await httpContext.Response.WriteAsJsonAsync(new
48-
{
49-
StatusCode = (int)HttpStatusCode.TooManyRequests,
50-
Error = "Too many requests",
51-
error.Reason,
52-
RetryAfter = error.RetryAfter.ToString()
53-
});
41+
await RateLimitResponseWriter.WriteTooManyRequestsAsync(httpContext, error);
5442
}
5543

5644
private RateLimitRequestContext CreateContext(HttpContext httpContext)
5745
{
5846
var endpoint = httpContext.GetEndpoint();
5947
var user = httpContext.User;
60-
var path = httpContext.Request.Path.Value ?? "/";
48+
var path = httpContext.Request.Path.Value ?? RateLimitMiddlewareConstants.DefaultPath;
6149

6250
return new RateLimitRequestContext
6351
{
64-
OperationName = endpoint?.DisplayName ?? $"{httpContext.Request.Method} {path}",
52+
OperationName = endpoint?.DisplayName ?? string.Concat(httpContext.Request.Method, RateLimitMiddlewareConstants.OperationNameSeparator, path),
6553
UserId = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.Identity?.Name,
66-
GroupId = user.FindFirstValue("group") ?? user.FindFirstValue("groups") ?? user.FindFirstValue(ClaimTypes.GroupSid),
67-
TenantId = user.FindFirstValue("tenant_id") ?? user.FindFirstValue("tid"),
54+
GroupId = user.FindFirstValue(RateLimitMiddlewareConstants.GroupClaimType)
55+
?? user.FindFirstValue(RateLimitMiddlewareConstants.GroupsClaimType)
56+
?? user.FindFirstValue(ClaimTypes.GroupSid),
57+
TenantId = user.FindFirstValue(RateLimitMiddlewareConstants.TenantIdClaimType)
58+
?? user.FindFirstValue(RateLimitMiddlewareConstants.ShortTenantIdClaimType),
6859
Role = user.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Role)?.Value,
6960
IpAddress = httpContext.Request.GetClientIpAddress(),
7061
Resource = path,
7162
Metadata = new Dictionary<string, string>
7263
{
73-
["method"] = httpContext.Request.Method,
74-
["path"] = path
64+
[RateLimitMiddlewareConstants.MethodMetadataKey] = httpContext.Request.Method,
65+
[RateLimitMiddlewareConstants.PathMetadataKey] = path
7566
},
7667
PolicyName = _policyName
7768
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace ManagedCode.Orleans.RateLimiting.Client.Middlewares;
2+
3+
internal static class RateLimitMiddlewareConstants
4+
{
5+
public const string DefaultPath = "/";
6+
public const string KeySeparator = ":";
7+
public const string OperationNameSeparator = " ";
8+
public const string ResourceSeparator = ".";
9+
public const string TooManyRequestsError = "Too many requests";
10+
public const string ConfigurationNotFoundLogMessage = "Configuration {ConfigurationName} not found for RateLimiter";
11+
public const string GroupClaimType = "group";
12+
public const string GroupsClaimType = "groups";
13+
public const string TenantIdClaimType = "tenant_id";
14+
public const string ShortTenantIdClaimType = "tid";
15+
public const string HubMetadataKey = "hub";
16+
public const string MethodMetadataKey = "method";
17+
public const string ConfigurationMetadataKey = "configuration";
18+
public const string PartitionMetadataKey = "partition";
19+
public const string PathMetadataKey = "path";
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Threading.Tasks;
2+
using ManagedCode.Orleans.RateLimiting.Core.Models;
3+
using Microsoft.AspNetCore.Http;
4+
5+
namespace ManagedCode.Orleans.RateLimiting.Client.Middlewares;
6+
7+
internal static class RateLimitResponseWriter
8+
{
9+
public static async Task WriteTooManyRequestsAsync(HttpContext httpContext, OrleansRateLimitLease lease)
10+
{
11+
if (httpContext.Response.HasStarted)
12+
return;
13+
14+
httpContext.Response.Clear();
15+
httpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
16+
await httpContext.Response.WriteAsJsonAsync(new RateLimitRejectedResponse(
17+
StatusCodes.Status429TooManyRequests,
18+
RateLimitMiddlewareConstants.TooManyRequestsError,
19+
lease.Reason,
20+
lease.RetryAfter.ToString()));
21+
}
22+
23+
private sealed record RateLimitRejectedResponse(int StatusCode, string Error, string Reason, string RetryAfter);
24+
}

ManagedCode.Orleans.RateLimiting.Client/Middlewares/RateLimitingHubFilter.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,23 @@ private RateLimitRequestContext CreateContext(HubInvocationContext invocationCon
6464
{
6565
OperationName = invocationContext.HubMethodName,
6666
UserId = userKey,
67-
GroupId = user?.FindFirstValue("group") ?? user?.FindFirstValue("groups") ?? user?.FindFirstValue(ClaimTypes.GroupSid),
68-
TenantId = user?.FindFirstValue("tenant_id") ?? user?.FindFirstValue("tid"),
67+
GroupId = user?.FindFirstValue(RateLimitMiddlewareConstants.GroupClaimType)
68+
?? user?.FindFirstValue(RateLimitMiddlewareConstants.GroupsClaimType)
69+
?? user?.FindFirstValue(ClaimTypes.GroupSid),
70+
TenantId = user?.FindFirstValue(RateLimitMiddlewareConstants.TenantIdClaimType)
71+
?? user?.FindFirstValue(RateLimitMiddlewareConstants.ShortTenantIdClaimType),
6972
Role = user?.FindFirstValue(ClaimTypes.Role),
7073
IpAddress = httpContext?.Connection.RemoteIpAddress?.ToString(),
71-
Resource = $"{invocationContext.Hub.GetType().FullName}.{invocationContext.HubMethodName}",
74+
Resource = string.Concat(
75+
invocationContext.Hub.GetType().FullName,
76+
RateLimitMiddlewareConstants.ResourceSeparator,
77+
invocationContext.HubMethodName),
7278
Metadata = new Dictionary<string, string>
7379
{
74-
["hub"] = invocationContext.Hub.GetType().FullName ?? invocationContext.Hub.GetType().Name,
75-
["method"] = invocationContext.HubMethodName,
76-
["configuration"] = _options.ConfigurationName,
77-
["partition"] = _options.PartitionKind.ToString()
80+
[RateLimitMiddlewareConstants.HubMetadataKey] = invocationContext.Hub.GetType().FullName ?? invocationContext.Hub.GetType().Name,
81+
[RateLimitMiddlewareConstants.MethodMetadataKey] = invocationContext.HubMethodName,
82+
[RateLimitMiddlewareConstants.ConfigurationMetadataKey] = _options.ConfigurationName,
83+
[RateLimitMiddlewareConstants.PartitionMetadataKey] = _options.PartitionKind.ToString()
7884
},
7985
PolicyName = _options.PolicyName
8086
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace ManagedCode.Orleans.RateLimiting.Client.Options;
2+
3+
public static class SignalRRateLimitingDefaults
4+
{
5+
public const string PolicyName = "SignalR";
6+
public const string ConfigurationName = "SignalR";
7+
public const string AnonymousUserKey = "anonymous";
8+
}

ManagedCode.Orleans.RateLimiting.Client/Options/SignalRRateLimitingOptions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ namespace ManagedCode.Orleans.RateLimiting.Client.Options;
44

55
public sealed class SignalRRateLimitingOptions
66
{
7-
public string PolicyName { get; set; } = "SignalR";
7+
public string PolicyName { get; set; } = SignalRRateLimitingDefaults.PolicyName;
88

9-
public string ConfigurationName { get; set; } = "SignalR";
9+
public string ConfigurationName { get; set; } = SignalRRateLimitingDefaults.ConfigurationName;
1010

1111
public RateLimitPartitionKind PartitionKind { get; set; } = RateLimitPartitionKind.User;
1212

13-
public string AnonymousUserKey { get; set; } = "anonymous";
13+
public string AnonymousUserKey { get; set; } = SignalRRateLimitingDefaults.AnonymousUserKey;
1414
}

ManagedCode.Orleans.RateLimiting.Core/Exceptions/RateLimitExceededException.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using System;
2+
using ManagedCode.Orleans.RateLimiting.Core.Models;
23

34
namespace ManagedCode.Orleans.RateLimiting.Core.Exceptions;
45

56
public class RateLimitExceededException : Exception
67
{
7-
public RateLimitExceededException() : base("Rate limit exceeded")
8+
public RateLimitExceededException() : base(RateLimitMetadataNames.RateLimitExceededReason)
89
{
9-
Reason = "Rate limit exceeded";
10+
Reason = RateLimitMetadataNames.RateLimitExceededReason;
1011
RetryAfter = TimeSpan.Zero;
1112
}
1213

@@ -16,9 +17,9 @@ public RateLimitExceededException(string reason) : base(reason)
1617
RetryAfter = TimeSpan.Zero;
1718
}
1819

19-
public RateLimitExceededException(TimeSpan retry) : base("Time limit exceeded")
20+
public RateLimitExceededException(TimeSpan retry) : base(RateLimitMetadataNames.TimeLimitExceededReason)
2021
{
21-
Reason = "Time limit exceeded";
22+
Reason = RateLimitMetadataNames.TimeLimitExceededReason;
2223
RetryAfter = retry;
2324
}
2425

@@ -30,4 +31,4 @@ public RateLimitExceededException(string reason, TimeSpan retry) : base(reason)
3031

3132
public string Reason { get; set; }
3233
public TimeSpan RetryAfter { get; set; }
33-
}
34+
}

ManagedCode.Orleans.RateLimiting.Core/Models/Holders/GroupLimiterHolder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace ManagedCode.Orleans.RateLimiting.Core.Models.Holders;
77

88
public class GroupLimiterHolder : IAsyncDisposable
99
{
10+
private const string AlreadyAcquiredMessage = "A limiter group can only be acquired once before it is disposed.";
11+
1012
private readonly List<LimiterEntry> _holders = [];
1113
private bool _acquired;
1214
private bool _disposed;
@@ -41,7 +43,7 @@ public bool AddLimiter(ILimiterHolder? holder)
4143
ObjectDisposedException.ThrowIf(_disposed, this);
4244

4345
if (_acquired)
44-
throw new InvalidOperationException("A limiter group can only be acquired once before it is disposed.");
46+
throw new InvalidOperationException(AlreadyAcquiredMessage);
4547

4648
for (var index = 0; index < _holders.Count; index++)
4749
{

0 commit comments

Comments
 (0)