feat(api): count upload frames separately and send Retry-After when rate limited
This commit is contained in:
1 parent
82d55a97f9
commit
96c4335cb5
2 files changed
+15
-3
No files matched your search
@@ -68,7 +68,7 @@ public static class WebDropApp
|
|||||||
"X-WD-Role", "X-WD-Endpoint", "X-WD-Cap", "X-WD-Max-Downloads", "X-WD-Account",
|
"X-WD-Role", "X-WD-Endpoint", "X-WD-Cap", "X-WD-Max-Downloads", "X-WD-Account",
|
||||||
"X-WD-Event-Id", "X-WD-Event-Kind", "X-WD-Event-Channel", "X-WD-Event-Domain",
|
"X-WD-Event-Id", "X-WD-Event-Kind", "X-WD-Event-Channel", "X-WD-Event-Domain",
|
||||||
"X-WD-Event-Epoch", "X-WD-Event-Frames", "X-WD-Event-Manifest", "X-WD-Event-Signature", "X-WD-Event-Target")
|
"X-WD-Event-Epoch", "X-WD-Event-Frames", "X-WD-Event-Manifest", "X-WD-Event-Signature", "X-WD-Event-Target")
|
||||||
.WithExposedHeaders("X-WD-Last-Seq")
|
.WithExposedHeaders("X-WD-Last-Seq", "Retry-After")
|
||||||
.SetPreflightMaxAge(TimeSpan.FromHours(1))));
|
.SetPreflightMaxAge(TimeSpan.FromHours(1))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,13 +76,17 @@ public static class WebDropApp
|
|||||||
{
|
{
|
||||||
r.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
r.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||||
// 限流状态只在内存里,按 IP 分区,不落盘
|
// 限流状态只在内存里,按 IP 分区,不落盘
|
||||||
r.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
|
r.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx => IsFrameUpload(ctx.Request)
|
||||||
RateLimitPartition.GetFixedWindowLimiter(ClientKey(ctx), _ => PerMinute(options.RequestsPerMinute)));
|
? RateLimitPartition.GetFixedWindowLimiter("frames:" + ClientKey(ctx), _ => PerMinute(options.FrameRequestsPerMinute))
|
||||||
|
: RateLimitPartition.GetFixedWindowLimiter(ClientKey(ctx), _ => PerMinute(options.RequestsPerMinute)));
|
||||||
r.AddPolicy(Endpoints.CreatePolicy, ctx =>
|
r.AddPolicy(Endpoints.CreatePolicy, ctx =>
|
||||||
RateLimitPartition.GetFixedWindowLimiter(ClientKey(ctx), _ => PerMinute(options.ChannelCreatesPerMinute)));
|
RateLimitPartition.GetFixedWindowLimiter(ClientKey(ctx), _ => PerMinute(options.ChannelCreatesPerMinute)));
|
||||||
r.OnRejected = async (context, ct) =>
|
r.OnRejected = async (context, ct) =>
|
||||||
{
|
{
|
||||||
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
|
||||||
|
// 告诉客户端多久后窗口重置,弱网和上传重试据此等待,不再盲目重试
|
||||||
|
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
|
||||||
|
context.HttpContext.Response.Headers.RetryAfter = Math.Max(1, (int)Math.Ceiling(retryAfter.TotalSeconds)).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
await context.HttpContext.Response.WriteAsJsonAsync(
|
await context.HttpContext.Response.WriteAsJsonAsync(
|
||||||
new ErrorBody { Error = ErrorCode.RateLimited.ToWire() }, ApiJson.Default.ErrorBody, cancellationToken: ct);
|
new ErrorBody { Error = ErrorCode.RateLimited.ToWire() }, ApiJson.Default.ErrorBody, cancellationToken: ct);
|
||||||
};
|
};
|
||||||
@@ -134,6 +138,9 @@ public static class WebDropApp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsFrameUpload(HttpRequest request) =>
|
||||||
|
HttpMethods.IsPost(request.Method) && request.Path.Value is { } path && path.EndsWith("/frames", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static string ClientKey(HttpContext ctx) => ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
private static string ClientKey(HttpContext ctx) => ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||||
|
|
||||||
private static FixedWindowRateLimiterOptions PerMinute(int permits) => new()
|
private static FixedWindowRateLimiterOptions PerMinute(int permits) => new()
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ public sealed class WebDropOptions
|
|||||||
public string? PasskeyRpId { get; set; }
|
public string? PasskeyRpId { get; set; }
|
||||||
|
|
||||||
public int RequestsPerMinute { get; set; } = 600;
|
public int RequestsPerMinute { get; set; } = 600;
|
||||||
|
/// <summary>
|
||||||
|
/// 上传帧(POST .../frames)单独计数。每帧最多约 1 MiB,和普通 API 请求放在一个 600/分钟的桶里,
|
||||||
|
/// 持续上传速度就被锁在约 10 MiB/s;帧的总量另有空间配额和单条上限保护。
|
||||||
|
/// </summary>
|
||||||
|
public int FrameRequestsPerMinute { get; set; } = 6000;
|
||||||
public int ChannelCreatesPerMinute { get; set; } = 30;
|
public int ChannelCreatesPerMinute { get; set; } = 30;
|
||||||
public int ShareDraftTtlSeconds { get; set; } = 86_400;
|
public int ShareDraftTtlSeconds { get; set; } = 86_400;
|
||||||
public int SweepIntervalSeconds { get; set; } = 30;
|
public int SweepIntervalSeconds { get; set; } = 30;
|
||||||
|
|||||||
Reference in new issue
Block a user