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:
2026-07-24 14:37:53 +03:00
parent 23b1a47c14
commit 43aa674e5b
4 changed files with 121 additions and 34 deletions

View File

@@ -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`, строки 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.

View File

@@ -50,6 +50,7 @@ Catalog of all wiki pages. One line per page, organized by type. Updated on ever
- [snolla-live-prod-inplace-image-bump](concepts/snolla-live-prod-inplace-image-bump.md) — переиспользуемый рецепт обновления образа на ЖИВОМ snolla-стеке VDS in-place (build→throwaway-staging-acceptance С VDS→env-preserving Portainer PUT→live-smoke); вкл. `put-stack.js`; отработан на тираже 0.42.1 (4 сайта: labtools/emspb/labtools.pro 2026-07-05 + tandemmebel 2026-07-12)
- [stostayer-web-deploy-runbook](concepts/stostayer-web-deploy-runbook.md) — деплой легаси web (`www.stostayer.ru`) на прод клиента: build→registry→Portainer-стек 16 через container-IP API с хоста; **БЛОКЕР: легаси не пересобрать с master (ESM-стена: top-level await @stostayer/data на build, ERR_REQUIRE_ESM @snollajs/snolla в рантайме)**; гоча — один push без retry (VPN-IP банится хостером)
- [stostayer-admin-minio-config](concepts/stostayer-admin-minio-config.md) — локальная .NET-админка stostayer (:8090) на stostayer MinIO: креды НЕ в pass (в stostayer.new config), endpoint `minio-api.stostayer.ru` (minio.stostayer.ru — не API-порт), region `us-west-1` (не local); fix 2026-07-22 — 3 замены в Web.config, админка=прод единое хранилище
- [stostayer-markdown-render-404-incident-2026-07](concepts/stostayer-markdown-render-404-incident-2026-07.md) — постмортем 2026-07-24: 404/500 на body_repair_service = двойной корень (snolla `markdown();` без присвоения + exact-pin-staleness — content-api `workspace:*` запёк `@snollajs/snolla: 0.7.4`, фиксовая 0.7.7 легла top-level, content-api юзал nested баговую). Фикс snolla 0.7.7/core 0.26.1 + content-api 0.8.2 `^0.7.7`; деплой 0.3.22 через pscp+load (без :443 push). Pre-build чек yarn.lock на single-snolla ловит класс. Cleanup `workspace-caret-interpackage-deps` (vitya scope)
- [proxy-debugging-test-the-real-client](concepts/proxy-debugging-test-the-real-client.md) — анти-паттерн: свои curl/standalone-тесты «работают», а боевой клиент пользователя нет; источник истины — реальный клиент
- [reality-pq-mldsa65-dest-incompatibility](concepts/reality-pq-mldsa65-dest-incompatibility.md) — REALITY+ML-DSA-65 (PQ) не работает с не-PQ dest (Akamai/intel шлёт HRR); fix = PQ-совместимый dest или выключить PQ; **caveat: снятие PQ ≠ рабочий Reality у GUI-клиентов**
- [recovery-architecture-snapshot](concepts/recovery-architecture-snapshot.md) — текущая recovery architecture (2026-05-19/21, attempt 2)

View File

@@ -110,3 +110,5 @@ Append-only log of wiki operations (ingests, promotions, lints, migrations).
## [2026-06-27] refactor | books-vds: документирован публичный bookva-db endpoint :33306 (+ slovo/bookva DB-exposure, mongo/ES internal) в § Доступ; закрыт wiki-drift по порту
## [2026-07-05] decision | тираж snolla 0.42.1 (live-prod in-place bumps). NEW concepts/snolla-live-prod-inplace-image-bump.md (переиспользуемый рецепт build→staging-acceptance-С-VDS→env-preserving Portainer PUT `put-stack.js`→live-smoke). UPDATE 3 рунбука секцией «0.42.1 in-place bump»: labtools-vds-deploy-runbook (стек 17, `labtools:566d41c`), emspb-vds-deploy-runbook (18, `emspb:95a5c42`), labtools.pro-vds-deploy-runbook (19, `labtools-pro:0610432`). FIX orphan: все 3 рунбука добавлены в index.md (не были каталогизированы). Tasks `[labtools-ru|emspb|labtools-pro-deploy-snolla-0-42-1]` 🟢.
## [2026-07-24] ingest | concepts/stostayer-markdown-render-404-incident-2026-07.md — постмортем инцидента 404/500 на body_repair_service (stostayer.ru). Двойной корень: (1) код-баг snolla `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. Фикс: snolla 0.7.7 + core 0.26.1 (код) + content-api 0.8.2 `^0.7.7` (упаковка). Деплой stostayer-web:0.3.22 через pscp SSH:20435 + sudo docker load + Portainer PUT pullImage:false (без push в :443, бана нет). Pre-build чек yarn.lock single-snolla ловит класс. index.md updated (+1). Cleanup `workspace-caret-interpackage-deps` — vitya scope. Memory: stostayer-deploy-image-via-pscp-load-avoid-443-push.md.