--- title: "1.24.27 — Metrics anonymous read surface hardened" description: "Three fixes on the anonymous /api/metrics/* read surface: a bounded date range (one request could previously materialise ~105M Redis keys), value/get answers 400 instead of an anonymous 500 that filed a level-12 incident per call, and a configured view permission on the public account is now actuall…" date: "2026-09-10" tags: ["release"] canonical: "https://django-mojo.sitesmojo.com/changelog/1-24-27/" --- Three fixes on the anonymous /api/metrics/* read surface: a bounded date range (one request could previously materialise ~105M Redis keys), value/get answers 400 instead of an anonymous 500 that filed a level-12 incident per call, and a configured view permission on the public account is now actually enforced. No breaking changes; the unconfigured public default stays open. ### Added - `METRICS_MAX_RANGE_BUCKETS` setting (default `10000`): the maximum number of time buckets a single metrics read may span. Checked inside the shared range helper before any key is generated, so every read endpoint (`fetch`, `series`, `value/get`, `categories`, `category_slugs`, `category_fetch`, `discover`) inherits it. `utils.count_buckets()` is the public estimator. - `value/get` accepts singular `slug` alongside `slugs`, matching `fetch`. ### Fixed - `value/get` with none of `slug` / `slugs` / `category` raised `UnboundLocalError`; the generic handler turned that into a level-12 `rest_error` incident with request data and a stack trace, once per request, from an endpoint an anonymous caller can reach. It is now a 400 `ValueException`. - `value/get` resolved category slugs from Redis before running `check_view_permissions`; the permission check now runs first. - `POST /api/metrics/permissions` with only one of `view_permissions` / `write_permissions` overwrote the other list with `[""]` (an absent key split to a truthy single empty string). Each list is written only when its key is present; an empty value clears it; the response reports stored state. ### Security - A date range wider than the cap is refused with a 400 rather than served. Previously `get_date_range` clamped nothing when both bounds were supplied, so one unauthenticated `fetch` at minute granularity over a century allocated many gigabytes of key strings before the MGET — a single-request memory-exhaustion vector on the open `public` account. Set the cap to `0` only on a host that is not internet-reachable. - A view permission configured on the `public` metrics account is now enforced on reads. `check_view_permissions` previously ended its chain before consulting `get_view_perms("public")`, so an operator locking public reads down got `{"status": true}` and no enforcement. When nothing is configured the account stays open, as before. ### Upgrade notes - No migration. The one behaviour change a client can notice is the range cap: a legitimate read wider than 10,000 buckets (about 7 days at minute granularity, 27 years at daily) now 400s and should narrow its window or raise the setting.