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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
---
|
||||
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`, строки 7–8:
|
||||
```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.
|
||||
Reference in New Issue
Block a user