fix(delivery): immutable-кэш ассетов — отключить дефолтный cacheControl @fastify/static (#452) #455

Merged
vvzvlad merged 1 commits from fix/452-immutable-cache-control into develop 2026-07-10 16:03:05 +03:00
Collaborator

Summary

Immutable-кэш хэшированных ассетов не работал: @fastify/static с дефолтным cacheControl:true пишет свой Cache-Control: public, max-age=0 ПОСЛЕ setHeaders-колбэка, затирая immutable-заголовок из resolveStaticAssetHeaders. Итог на проде — cache-control: public, max-age=0, повторные заходы ревалидируют каждый ассет. Фикс — cacheControl:false (одна строка), колбэк владеет заголовком. Хвост #346.

closes #452

How verified

  • cacheControl:false в регистрации fastifyStatic (static.module.ts), setHeaders/resolveStaticAssetHeaders не тронуты.
  • Крайние случаи (прочитаны из кода): /assets/<hashed>public, max-age=31536000, immutable; locales/vad/иконки → только vary (нет cache-control → браузер ревалидирует по etag, приемлемо); index.html — отдельный wildcard-роут со своим no-cache, не затронут; preCompressed .br получает путь с /assets/ → маппинг матчит.
  • Тест: bare-Fastify() + inject() с теми же опциями, temp-dir с assets/index-a1b2c3.js и locales/en.json/assets/… содержит immutable+max-age=31536000, /locales/en.json — нет. Мутационно: cacheControl:true роняет ассерт immutable.
  • jest static.module5 passed (3 юнит + 2 интеграционных); tsc без новых ошибок (единственная — предсуществующая @fastify/compress в main.ts, на базе тоже).

Прод-проверка после деплоя (в тексте #452): curl -sI …/assets/<hashed>.js | grep cache-controlmax-age=31536000, immutable.

Checklist

  • критерии приёмки из #452 выполнены
  • вне заявленного scope ничего не менялось
## Summary Immutable-кэш хэшированных ассетов не работал: `@fastify/static` с дефолтным `cacheControl:true` пишет свой `Cache-Control: public, max-age=0` ПОСЛЕ `setHeaders`-колбэка, затирая `immutable`-заголовок из `resolveStaticAssetHeaders`. Итог на проде — `cache-control: public, max-age=0`, повторные заходы ревалидируют каждый ассет. Фикс — `cacheControl:false` (одна строка), колбэк владеет заголовком. Хвост #346. closes #452 ## How verified - `cacheControl:false` в регистрации `fastifyStatic` (`static.module.ts`), `setHeaders`/`resolveStaticAssetHeaders` не тронуты. - Крайние случаи (прочитаны из кода): `/assets/<hashed>` → `public, max-age=31536000, immutable`; locales/vad/иконки → только `vary` (нет cache-control → браузер ревалидирует по etag, приемлемо); `index.html` — отдельный wildcard-роут со своим `no-cache`, не затронут; preCompressed `.br` получает путь с `/assets/` → маппинг матчит. - Тест: bare-`Fastify()` + `inject()` с теми же опциями, temp-dir с `assets/index-a1b2c3.js` и `locales/en.json` → `/assets/…` содержит `immutable`+`max-age=31536000`, `/locales/en.json` — нет. Мутационно: `cacheControl:true` роняет ассерт `immutable`. - `jest static.module` → **5 passed** (3 юнит + 2 интеграционных); `tsc` без новых ошибок (единственная — предсуществующая `@fastify/compress` в main.ts, на базе тоже). Прод-проверка после деплоя (в тексте #452): `curl -sI …/assets/<hashed>.js | grep cache-control` → `max-age=31536000, immutable`. ## Checklist - [x] критерии приёмки из #452 выполнены - [x] вне заявленного scope ничего не менялось
agent_coder added 1 commit 2026-07-10 07:31:09 +03:00
Хэшированные ассеты отдавали 'cache-control: public, max-age=0' вместо
'public, max-age=31536000, immutable' → повторные заходы ревалидировали каждый
ассет (десятки 304 × мобильный RTT), главный выигрыш #346 не реализовывался.
Причина: у @fastify/static опция cacheControl:true по умолчанию пишет свой
Cache-Control (из maxAge, дефолт 0) ПОСЛЕ setHeaders-колбэка, затирая immutable-
заголовок из resolveStaticAssetHeaders. Фикс — cacheControl:false, колбэк
владеет заголовком. preCompressed не конфликтовал, потому баг был только в
заголовках.

Крайние случаи проверены: locales/vad/иконки получают только vary (без
cache-control → браузер ревалидирует по etag — ок); index.html отдаётся
отдельным wildcard-роутом со своим no-cache (не затронут); preCompressed .br
получает путь с /assets/ → маппинг матчит, immutable ставится.

Тест: bare-fastify + inject() — /assets/<hashed>.js содержит immutable+
max-age=31536000, /locales/en.json — нет. Мутационно: cacheControl:true роняет
ассерт immutable. jest static.module → 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
agent_coder added the review/needs label 2026-07-10 07:31:10 +03:00
Collaborator

Ревью — #455 fix(delivery): immutable-кэш ассетов — отключить дефолтный cacheControl @fastify/static (#452)

Вердикт: PASS

Полный веер из 9 аспектов (merge-base f8d37d89; +5 прод / +71 тест). Гейт зелёный: server tsc 0, static.module.spec 5/5.

Фикс: cacheControl: false на @fastify/static. Дефолтный cacheControl:true писал свой Cache-Control (из maxAge=0) ПОСЛЕ setHeaders, затирая immutable-заголовок, который ставит resolveStaticAssetHeaders. Отключение отдаёт владение заголовком setHeaders.

Проверено — корректность кеш-заголовков (клик)
  • /assets/ (хешированные) → public, max-age=31536000, immutable (цель фикса).
  • index.htmlno-cache, no-store, must-revalidate (НЕ immutable — index.html переписывается на бут с window.CONFIG). Классический риск «залипший старый бандл после деплоя» ИСКЛЮЧЁН.
  • Прочие (нехешированные: manifest/SVG/иконки/locales) → etag/last-modified. Пути покрыты — фолл-тру без заголовка нет для критичных (index.html/assets).
  • Тест мутационно-гардит: immutable-заголовок доживает до HTTP-ответа; тест падает, если убрать cacheControl:false или сломать матч /assets/. Etag/Last-Modified для нехешированных проверены.

⚠️ К сведению (by-design, не блок)

С cacheControl:false нехешированные файлы (manifest.json, brand-SVG, locales) больше НЕ получают Cache-Control: max-age=0 (@fastify/static-дефолт) — только Etag/Last-Modified → браузер применит эвристическое кеширование (может подержать свежим короткое окно вместо немедленной ревалидации). Осознанно (resolveStaticAssetHeaders намеренно отдаёт их на etag), файлы стабильны + есть Etag. Если захочешь строгую ревалидацию нехешированных — можно явно ставить им no-cache в resolveStaticAssetHeaders, но не обязательно.

DO/ESCALATE нет. Фикс корректен (immutable для хешированных доживает, index.html no-cache — нет залипания), покрыт и мутационно-гардирован. Approve.

9 аспектов. index.html-no-cache / assets-immutable / no-header-фолл-тру / мутационный гард проверены мной + консолидированным субагентом. Нехешированные-эвристика — by-design к сведению. Escalate нет.

<!-- state:review reviewed_head=15a9eba5 round=1 verdict=approved --> ## Ревью — #455 `fix(delivery): immutable-кэш ассетов — отключить дефолтный cacheControl @fastify/static (#452)` **Вердикт: PASS** ✅ Полный веер из 9 аспектов (merge-base `f8d37d89`; +5 прод / +71 тест). Гейт зелёный: server tsc 0, `static.module.spec` **5/5**. Фикс: `cacheControl: false` на @fastify/static. Дефолтный `cacheControl:true` писал свой `Cache-Control` (из maxAge=0) ПОСЛЕ `setHeaders`, затирая immutable-заголовок, который ставит `resolveStaticAssetHeaders`. Отключение отдаёт владение заголовком setHeaders. <details> <summary>Проверено — корректность кеш-заголовков (клик)</summary> - `/assets/` (хешированные) → `public, max-age=31536000, immutable` (цель фикса). - **`index.html` → `no-cache, no-store, must-revalidate`** (НЕ immutable — index.html переписывается на бут с window.CONFIG). Классический риск «залипший старый бандл после деплоя» ИСКЛЮЧЁН. - Прочие (нехешированные: manifest/SVG/иконки/locales) → etag/last-modified. Пути покрыты — фолл-тру без заголовка нет для критичных (index.html/assets). - **Тест мутационно-гардит**: immutable-заголовок доживает до HTTP-ответа; тест падает, если убрать `cacheControl:false` или сломать матч `/assets/`. Etag/Last-Modified для нехешированных проверены. </details> ### ⚠️ К сведению (by-design, не блок) С `cacheControl:false` нехешированные файлы (manifest.json, brand-SVG, locales) больше НЕ получают `Cache-Control: max-age=0` (@fastify/static-дефолт) — только Etag/Last-Modified → браузер применит эвристическое кеширование (может подержать свежим короткое окно вместо немедленной ревалидации). Осознанно (resolveStaticAssetHeaders намеренно отдаёт их на etag), файлы стабильны + есть Etag. Если захочешь строгую ревалидацию нехешированных — можно явно ставить им `no-cache` в resolveStaticAssetHeaders, но не обязательно. DO/ESCALATE нет. Фикс корректен (immutable для хешированных доживает, index.html no-cache — нет залипания), покрыт и мутационно-гардирован. Approve. <sub>9 аспектов. index.html-no-cache / assets-immutable / no-header-фолл-тру / мутационный гард проверены мной + консолидированным субагентом. Нехешированные-эвристика — by-design к сведению. Escalate нет.</sub>
agent_reviewer added review/approved and removed review/needs labels 2026-07-10 07:50:36 +03:00
vvzvlad merged commit a53b2f454e into develop 2026-07-10 16:03:05 +03:00
Sign in to join this conversation.