From 1a06262d5c08cefc390694b02634efbfe7a28ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vedran=20Bilopavlovi=C4=87?= Date: Tue, 2 Jun 2026 13:46:52 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20add=20v3.16.2=20changelog=20=E2=80=94?= =?UTF-8?q?=20per-policy=20rate-limiter=20StatusCode/StatusMessage=20overr?= =?UTF-8?q?ides?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide/changelog/index.md | 3 ++ docs/guide/changelog/v3.16.2.md | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 docs/guide/changelog/v3.16.2.md diff --git a/docs/guide/changelog/index.md b/docs/guide/changelog/index.md index 277bc40..715fd88 100644 --- a/docs/guide/changelog/index.md +++ b/docs/guide/changelog/index.md @@ -10,9 +10,12 @@ Note: The changelog for versions older than 3.0 can be found here: [Changelog Ar | Version | Date | |---------|------| +| [v3.16.2](/guide/changelog/v3.16.2) | 2026-06-02 | | [v3.16.1](/guide/changelog/v3.16.1) | 2026-06-01 | | [v3.16.0](/guide/changelog/v3.16.0) | 2026-05-20 | +- New: rate-limiter rejection `StatusCode`/`StatusMessage` are now overridable per policy (the global values stay as defaults); ships a ready-to-use disabled `login_throttle` policy + - Fix: cache stampede protection now actually fires for cached routine responses (`IRoutineCache.GetOrCreateAsync`); a burst of identical cold-cache requests collapses to a single database execution - Fix: JSON-to-parameter parsers for `timestamp`, `timestamptz`, `time`, and `timetz` are now host-TZ-independent (silent host-offset shift removed) diff --git a/docs/guide/changelog/v3.16.2.md b/docs/guide/changelog/v3.16.2.md new file mode 100644 index 0000000..3025420 --- /dev/null +++ b/docs/guide/changelog/v3.16.2.md @@ -0,0 +1,68 @@ +# Changelog v3.16.2 (2026-06-02) + +## Version [3.16.2](https://github.com/NpgsqlRest/NpgsqlRest/tree/3.16.2) (2026-06-02) + +[Full Changelog](https://github.com/NpgsqlRest/NpgsqlRest/compare/3.16.1...3.16.2) + +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`.