Files
admin/.wiki/concepts/stostayer-markdown-render-404-incident-2026-07.md
vitya 43aa674e5b wiki(stostayer): ingest markdown-render-404 incident postmortem + NEXT_SESSION
Постмортем инцидента 2026-07-24: 404/500 на body_repair_service (stostayer.ru) =
двойной корень. (1) код-баг snolla: viewModels/index.js `import markdown from
'markdown-it'; markdown();` — инстанс выброшен, markdown.render undefined, краш на
markdown-поле-шаблонах (латентный ~3 мес с коммита 1e3f897 2026-04-21). (2) упаковка
exact-pin-staleness: @snollajs/content-api@0.8.0 `workspace:*` запёк
@snollajs/snolla:0.7.4 exact → фиксовая 0.7.7 легла top-level, content-api юзал
nested баговую 0.7.4. yarn resolutions/patch не форсят exact-pin опубликованного
пакета — решено публикацией @snollajs/content-api@0.8.2 (^0.7.7).

Деплой stostayer-web:0.3.22 через pscp SSH:20435 + sudo docker load + Portainer PUT
pullImage:false (без push в docker.stostayer.ru:443, бана нет). 4 URL → 200 публично.
Pre-build чек yarn.lock single-snolla ловит класс. Cleanup
workspace-caret-interpackage-deps — за vitya+snolla.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 14:37:53 +03:00

9.8 KiB
Raw Blame History

title: stostayer 404/500 на body_repair_service — инцидент markdown.render + exact-pin-staleness (2026-07-24) status: resolved tags: [stostayer, snolla, postmortem, packaging, yarn, deploy, markdown-it] related: stostayer-web-deploy-runbook, snolla-live-prod-inplace-image-bump updated: 2026-07-24

stostayer 404/500 на body_repair_service — двойной корень

Постмортем инцидента: страницы шаблона body_repair_service на www.stostayer.ru отдавали 404 (фронт) / 500 (content-api). Починено 2026-07-24. Корень двойной — код-баг snolla + упаковка-баг (exact-pin-staleness).

Симптом

https://www.stostayer.ru/kuzovnoj-malyarnij-remont/{zamena-elementov,rihtovka-kuzova,remont-bamperov,pokraska} → 404
GET /snolla/pages/viewModel?path=.../zamena-elementov → 500 { "message": "markdown.render is not a function" }
GET /snolla/pages?path=/kuzovnoj-malyarnij-remont/&template=body_repair_service → 500

Родитель /kuzovnoj-malyarnij-remont (шаблон body_repair, без markdown-поля) → 200. В БД страницы есть, Published=true, Path совпадает точно — 404 это не данных, это следствие 500 (фронт packages/web/store/snolla.js ловит 500, коммитит page=null, _.vue рендерит 404).

Корень 1 — код-баг snolla (markdown.render is not a function)

@snollajs/snollalib/viewModels/index.js, строки 78:

import markdown from 'markdown-it';
markdown();          // фабрику вызвали, инстанс ВЫБРОШЕН
// ...
viewModel[name] = value ? markdown.render(value) : null;   // markdown — конструктор, .render нет → TypeError

Краш в fillViewModel на шаблонах с markdown-полем (markItUpMarkdownEditorTemplate / markdownLiteralTemplate). body_repair без такого поля → 200; body_repair_service с markdown-полем → краш. Латентный ~3 мес — ломается только при заходе на markdown-поле-страницу, никто не замечал.

Фикс (snolla): import markdownit from 'markdown-it'; const markdown = markdownit();markdown.render работает. snolla выпустила @snollajs/snolla@0.7.7 (0.7.x legacy-line) + @snollajs/core@0.26.1 (master, + TDD fillViewModel.markdown.test.js, 259/259 green). Верифицировано: markdown-it 12.3.2 (snolla's nested) и 14.1.0 (root) — обе дают инстанс с .render через markdownit().

Происхождение: предмонорепо-ный Node-snolla юзал другой markdown-движок (работало). Коммит 1e3f897 (2026-04-21, импорт в монорепо) сменил на markdown-it без регресс-прогона по field-типам → markdown(); без присвоения. Класс «портовая опечатка при смене либы без регресса по field-типам».

Корень 2 — упаковка (exact-pin-staleness) — почему фикс НЕ дошёл до прода первым деплоем

@snollajs/content-api@0.8.0 (servит /snolla/pages) в routes/pages.js делает import { ... } from '@snollajs/snolla'. В опубликованном content-api@0.8.0 его package.json жёстко: "@snollajs/snolla": "0.7.4"exact-пин, без ^.

Откуда: в монорепо snolla у content-api было "@snollajs/snolla": "workspace:*". При publish yarn разрешает workspace:* в точную текущую версию и запекает её константным пином. В момент публикации content-api 0.8.0 snolla была 0.7.4 → в мир ушёл content-api с залитым 0.7.4. Позже snolla выпустила фиксовую 0.7.7, но content-api 0.8.0 об этом не знает — навсегда просит 0.7.4.

Дедуп-ловушка: бамп @snollajs/snolla^0.7.7 в packages/web (top-level) НЕ поднимает content-api's exact-пин. Yarn ставит две копии:

  • node_modules/@snollajs/snolla = 0.7.7 (top-level, фиксовая) — зовётся SSR-стороной Nuxt
  • node_modules/@snollajs/content-api/node_modules/@snollajs/snolla = 0.7.4 (nested, баговая) — её и зовёт content-api's routes/pages.js (ближайшая резолвится)

content-api бегёт на баговой 0.7.4 → краш. Деплой 0.3.21 (snolla 0.7.7 top-level) не починил ничего (регрессии нет — 0.3.20 тоже 500).

Почему yarn resolutions НЕ спас (вариант B — мёртв)

  • resolutions в packages/web/package.json — yarn berry игнорирует workspace-resolutions (читает только root). → нет эффекта.
  • root resolutions: { "@snollajs/snolla": "0.7.7" } (blanket) и scoped "@snollajs/content-api/@snollajs/snolla": "0.7.7"не форсят exact-пин, объявленный внутри чужого package.json. yarn всё равно резолвит content-api в 0.7.4 по его package.json (stostayer.new эмпирически проверил, incl. patch на content-api — не держится).

Вывод: resolutions/patch не могут переписать exact-pin опубликованного пакета. Только новая публикация пакета с фиксовой зависимостью.

Фикс упаковки (вариант A — единственный)

snolla опубликовала @snollajs/content-api@0.8.2 с "@snollajs/snolla": "^0.7.7" (в монорепо workspace:*workspace:^, запечётся ^0.7.7). 0.8.1 уже был на verdaccio с запечённой 0.7.4 → нужна именно 0.8.2 (перепубликация той же версии = тот же 0.7.4). stostayer.new бампит @snollajs/content-api^0.8.2 в packages/web → yarn: content-api ^0.7.7 ∩ web ^0.7.7одна 0.7.7 top-level, nested 0.7.4 уходит. content-api зовёт фиксовую snolla.

Деплой 0.3.22 (без push в :443 — см. stostayer-deploy-image-via-pscp-load)

Образ ~3.3GB. Push в docker.stostayer.ru:443 банит VPN-egress (см. stostayer-web-deploy-runbook гоча). Транспорт без :443:

  1. Build локально offline (.yarnrc.ymlenableNetwork:false + убрать npmAuthToken; .yarn/cache с 0.8.2+0.7.7).
  2. docker save | gzip -1 → ~0.8GB (sw-0.3.22.tar.gz, shared-layers).
  3. pscp по SSH:20435 на хост /tmp (~10MB/s, ~80s) — SSH не банится, только :443.
  4. На хосте: echo pass | sudo -S bash -c 'gunzip -c /tmp/sw-0.3.22.tar.gz | docker load' (sudo -S + inner pipe из файла — конфликта stdin нет).
  5. Portainer PUT стека 16 pullImage:false (образ уже локально, НЕ тянуть из :443). Payload через -d @/tmp/payload.json (jq в файл) — НЕ встраивать JSON в double-quoted bash -c (кавычки payload ломают → 500).
  6. Verify: docker logsREADY Server listening; curl -H "Host: www.stostayer.ru" http://localhost:3000/<page> + публичный https://www.stostayer.ru/<page> → 200.

Pre-build чек (ловит exact-pin-staleness до деплоя): перед build'ом grep -E "^\"@snollajs/snolla@npm:" yarn.lock → ожидать ОДНУ версию (фиксовую), без баговой. В собранном образе: docker run --rm --entrypoint sh <img> -c 'find /usr/src/app -path "*@snollajs/snolla/package.json"' → одна копия 0.7.7. Без этого чека первый деплой 0.3.21 прошёл «успешно» но не починил.

Outcome

4 URL → 200 публично через Angie/TLS. 0.3.22 на проде (stostayer-web стек 16). Код-фикс (snolla 0.7.7 + core 0.26.1) + упаковка (content-api 0.8.2 ^0.7.7). stostayer.new коммит ace20a1.

Cleanup (open, не блокирует — за vitya)

snolla-таска workspace-caret-interpackage-deps: перевести workspace:*workspace:^ по монорепо, чтоб класс exact-pin-staleness не повторялся. Scope-вопрос: covers ли 0.7.x legacy-line content-api или только master. Отдельно: 0.7.x content-api не получил peerDep-паттерн (master-only, 0.12.0) — переносить ли зависит от срока жизни 0.7.x vs миграции stostayer на современную линию.

Multi-tenant находка (факультатив, snolla своп на kupimknigi): краш бьёт любой шаблон с markdown-полем на любой БД snolla (fillViewModel общий). На shared MoreThenCms-БД kupimknigi нашлись 2 чужих siteId (919219B3, 5A826EB9, шаблоны article_markdown/test) — если они на snolla 0.7.x, тоже крашились; фикс закроет при их пересборке.

Связи