Skip to content
Written with Claude

Changelog v3.16.2 (2026-06-02)

Version 3.16.2 (2026-06-02)

Full Changelog

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
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
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.

Comments