Changelog v3.16.2 (2026-06-02)
Version 3.16.2 (2026-06-02)
Patch release that makes the rate-limiter rejection status code and message overridable per policy. Previously RateLimiterOptions:StatusCode and RateLimiterOptions:StatusMessage were the only values returned for a rejected request, applied globally regardless of which policy tripped. A config like a login_throttle policy with the message "Too many login attempts…" would return that same login-specific text for every rate-limited endpoint, even ones that have nothing to do with logins.
What changed
Per-policy StatusCode / StatusMessage overrides
Each named policy under RateLimiterOptions:Policies may now set its own StatusCode and/or StatusMessage:
jsonc
"RateLimiterOptions": {
"Enabled": true,
"StatusCode": 429, // global default
"StatusMessage": "Too many requests. Please slow down.",
"Policies": {
"login_throttle": {
"Type": "FixedWindow",
"PermitLimit": 10,
"WindowSeconds": 60,
"StatusMessage": "Too many login attempts. Please wait a minute and try again.",
"Partition": { "Sources": [ { "Type": "IpAddress" } ] }
},
"api": {
"Type": "TokenBucket",
"StatusCode": 503,
"StatusMessage": "API capacity reached. Retry shortly."
}
}
}A request rejected by a given policy now returns that policy's status code and message; a policy that omits either field inherits the global value. The override that applies is resolved at rejection time from the endpoint's rate-limiter policy name, so it is correct even though ASP.NET Core exposes only a single global OnRejected/RejectionStatusCode.
This is fully backward compatible: configs that set only the global StatusCode/StatusMessage behave exactly as before — those values simply become the defaults that policies may override.
New ready-to-use login_throttle default policy
The shipped appsettings.json now includes a disabled ("Enabled": false) login_throttle policy — 10 attempts per minute partitioned per client IP, with its own rejection message — so the common case is one flag away:
jsonc
"login_throttle": {
"Type": "FixedWindow",
"Enabled": false,
"PermitLimit": 10,
"WindowSeconds": 60,
"QueueLimit": 0,
"AutoReplenishment": true,
"StatusMessage": "Too many login attempts. Please wait a minute and try again.",
"Partition": { "Sources": [ { "Type": "IpAddress" } ], "BypassAuthenticated": false }
}Apply it to a login endpoint with the rate_limiter login_throttle comment annotation (or set it as DefaultPolicy).
Test coverage
NpgsqlRestTests/AuthTests/RateLimiterPerPolicyTests.cs (fixture RateLimiterPerPolicyTestFixture) boots the limiter through the same wiring BuildRateLimiter emits and drives the real Builder.ApplyRateLimiterRejectionAsync helper over HTTP, asserting:
- a policy with a message-only override returns its own message but the global status code,
- a policy overriding both returns its own status code (503) and message,
- a policy with no override inherits the global status code and message.
Config-key validation for the new per-policy StatusCode/StatusMessage keys is covered in ConfigTests/ConfigValidationTests.cs.