- ConditionalGetMiddleware added to MIDDLEWARE (ETag/304 for all views)
- @condition(etag_func, last_modified_func) on MateriaLegislativa and
NormaJuridica detail views — skips view execution on cache hit via
data_ultima_atualizacao (auto_now=True) as freshness signal
- nginx /static/: expires 90m + Cache-Control public, max-age=5400
- nginx: removed upload-endpoint special-casing (location ~* ^/(protocoloadm/criar-protocolo|...))
- plan/RATE-LIMITER-PLAN.md updated to reflect all Phase 7 changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| Redis topology | **Single pod** (no Sentinel, no Cluster) | 65 MB of active data fits comfortably; cluster complexity not justified |
| PDF caching in Redis | **No** — ETags + sendfile are sufficient | Once rate limiting + ETags are active, repeat requests become 304s with zero bytes transferred |
| HTTP conditional requests | **`ConditionalGetMiddleware` + `@condition` decorator** | `ConditionalGetMiddleware` handles ETag/304 for all views; `@condition(etag_func, last_modified_func)` on materia/norma detail views skips view execution entirely on cache hit |
| Upload endpoint special-casing (nginx) | **Removed** — fall through to `location /` | No justification for separate `limit_req` zone; `location /` with `sapl_general` covers it |
| Static asset cache policy | **90 min** (`expires 90m`, `max-age=5400`) | Conservative — safe with `collectstatic` content-hashed filenames; `immutable` not used (would require verified forever-hashed URLs) |
| Rate-limit enforcement | **Django middleware** with shared Redis | No nginx image changes required; solves cross-pod consistency immediately |
# Internal — only reachable via X-Accel-Redirect, not by external clients.
location /_accel/media/ {
location /private/media/ {
internal;
alias /var/interlegis/sapl/media/;
sendfile on;
@ -755,6 +765,8 @@ location /_accel/media/ {
}
```
Upload endpoints (`/protocoloadm/criar-protocolo`, `/materia/.*upload`, `/norma/.*upload`) no longer have a dedicated `location` block — they fall through to `location /` which applies the `sapl_general` zone.
### Django view (`sapl/base/media.py`)
`serve_media(request, path)` — registered at `^media/(?P<path>.*)$` in `sapl/urls.py`.
2. **Auth gate** — `documentos_privados/` paths require an authenticated session; redirects to login otherwise.
3. **Path counter** — increments `rl:{ns}:path:{sha256}:reqs` in Redis DB 1 (TTL = `MEDIA_PATH_COUNTER_TTL`).
4. **Serve** — in DEBUG: `django.views.static.serve` directly. In production: `X-Accel-Redirect: /_accel/media/<path>`. Nginx sets `Content-Type` from its own `mime.types`.
4. **Serve** — in DEBUG: `django.views.static.serve` directly. In production: `X-Accel-Redirect: /private/media/<path>`. Nginx sets `Content-Type` from its own `mime.types`.
### Settings
@ -957,6 +969,56 @@ class PesquisarMateriaView(FilterView):
---
## HTTP Conditional Requests
Two complementary mechanisms eliminate redundant work for unchanged content.
### `ConditionalGetMiddleware` (all views)
Added to `MIDDLEWARE` in `sapl/settings.py` (after `CommonMiddleware`). For every
Django response it:
1. Generates a weak `ETag` from an MD5 of the response body if none is set.
2. Compares against the client's `If-None-Match` / `If-Modified-Since`.
3. Returns `304 Not Modified` (no body) on a match.
4. Handles `HEAD` requests by stripping the body and keeping headers.
**Caveat**: the view still executes and renders before the check fires. The saving
is bandwidth, not CPU/DB work.
### `@condition` decorator — materia and norma detail views
For `MateriaLegislativaCrud.DetailView` and `NormaCrud.DetailView` a cheap