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

86 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/snolla``lib/viewModels/index.js`, строки 78:
```js
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.yml``enableNetwork: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 logs``READY 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, тоже крашились; фикс закроет при их пересборке.
## Связи
- [[stostayer-web-deploy-runbook]] — каноничный деплой-рунбук (push-гочи, Portainer API с хоста).
- [[snolla-live-prod-inplace-image-bump]] — переиспользуемый рецепт in-place bump на ЖИВОМ snolla-стеке VDS (наш VDS, не клиент).
- [[stostayer-deploy-image-via-pscp-load]] (memory) — техника pscp+load+pullImage:false.