[bug][ai-chat][server] POST /ai-chat/bound-chat роняет 500 (Postgres 22P02): клиент шлёт slugId страницы в UUID-колонку — привязка чата к документу молча ломается #312

Open
opened 2026-07-03 17:54:30 +03:00 by agent_vscode · 0 comments
Collaborator

Симптом

При открытии страницы POST /api/ai-chat/bound-chat падает с 500:

ERR /api/ai-chat/bound-chat context=ExceptionsHandler
err PostgresError 22P02: invalid input syntax for type uuid: "i82qXsivsx"
  where: unnamed portal parameter $3 = '...'  file uuid.c routine string_to_uuid
  at ErrorResponse (postgres/cjs/src/connection.js:815:30)

"i82qXsivsx" — это slugId страницы (10-символьный nanoid), а не UUID.

Причина (цепочка)

Клиент отправляет в bound-chat slugId под именем pageId, сервер кладёт его в запрос по UUID-колонке:

  1. apps/client/src/features/ai-chat/hooks/use-open-ai-chat.ts:30 — берёт slug из URL и, вводя в заблуждение именем, зовёт его pageId:
    const pageId = extractPageSlugId(match?.params?.pageSlug); // "pppp-5mMn4EFZTw" → "5mMn4EFZTw" (slugId, НЕ uuid)
    
    extractPageSlugId (apps/client/src/lib/utils.tsx:14-23) для не-UUID возвращает хвост после последнего дефиса — короткий slugId.
  2. apps/client/src/features/ai-chat/services/ai-chat-service.ts:49-53getBoundChat(pageId)POST /ai-chat/bound-chat { pageId: <slugId> }.
  3. apps/server/src/core/ai-chat/dto/ai-chat.dto.ts:41-44BoundChatDto.pageId валидируется только как @IsString(), без UUID-проверки, поэтому slugId проходит.
  4. apps/server/src/core/ai-chat/ai-chat.controller.ts:79-91boundChataiChatRepo.findLatestByPage(user.id, workspace.id, dto.pageId).
  5. apps/server/src/database/repos/ai-chat/ai-chat.repo.ts:91.where('pageId', '=', pageId) по UUID-колонке pageId → Postgres 22P02 → 500.

Impact

  • Функция молча не работает. Клиент ловит ошибку fail-soft (use-open-ai-chat.ts:46-48, catch { resolved = null }), поэтому при открытии страницы вместо «последнего чата этой страницы» открывается новый. Пользователь тихо теряет привязку чата к документу.
  • 500 + стек в логах на каждое открытие страницы со slug-URL (а это все обычные ссылки на страницы) — постоянный шум уровня ERR.

Severity: medium (не крашит UX, но фича сломана и логи засоряются 500-ками).

Фикс

Резолвить входящий id в реальный UUID страницы на сервере, т.к. PageRepo.findById (apps/server/src/database/repos/page/page.repo.ts:65, ветка isValidUUID на стр. 117-121) уже принимает и UUID, и slugId:

// ai-chat.controller.ts, boundChat(...)
const page = await this.pageRepo.findById(dto.pageId); // принимает slugId ИЛИ uuid
if (!page || page.workspaceId !== workspace.id) return { chatId: null };
const chat = await this.aiChatRepo.findLatestByPage(user.id, workspace.id, page.id); // page.id — настоящий uuid
return { chatId: chat?.id ?? null };

Дополнительно:

  • Переименовать на клиенте pageIdslugId в use-open-ai-chat.ts/getBoundChat, чтобы убрать семантическую путаницу (в поле лежит slugId).
  • Только @IsUUID() в DTO — недостаточно: это лишь превратит 500 в 400, но привязку чата всё равно сломает (клиент шлёт slugId). Нужен резолв slug→id.

Acceptance / DoD

  • POST /ai-chat/bound-chat со slug-URL возвращает 200 { chatId } и корректно находит последний чат страницы; 500/22P02 в логах нет.
  • Открытие страницы, где ранее был чат, переоткрывает именно его, а не создаёт новый.
  • Тест на резолв slugId→pageId в bound-chat (и на отсутствие/чужой воркспейс → { chatId: null }, без утечки).

Окружение

  • Версия: server@0.94.1 (docmost@0.94.1)
  • Эндпоинт: POST /api/ai-chat/bound-chat
  • Дата наблюдения: 2026-07-03 ~14:11 UTC
  • Клиент: Safari 17.6 / macOS
## Симптом При открытии страницы `POST /api/ai-chat/bound-chat` падает с 500: ``` ERR /api/ai-chat/bound-chat context=ExceptionsHandler err PostgresError 22P02: invalid input syntax for type uuid: "i82qXsivsx" where: unnamed portal parameter $3 = '...' file uuid.c routine string_to_uuid at ErrorResponse (postgres/cjs/src/connection.js:815:30) ``` `"i82qXsivsx"` — это **slugId** страницы (10-символьный nanoid), а не UUID. ## Причина (цепочка) Клиент отправляет в `bound-chat` **slugId** под именем `pageId`, сервер кладёт его в запрос по UUID-колонке: 1. `apps/client/src/features/ai-chat/hooks/use-open-ai-chat.ts:30` — берёт slug из URL и, вводя в заблуждение именем, зовёт его `pageId`: ```tsx const pageId = extractPageSlugId(match?.params?.pageSlug); // "pppp-5mMn4EFZTw" → "5mMn4EFZTw" (slugId, НЕ uuid) ``` `extractPageSlugId` (`apps/client/src/lib/utils.tsx:14-23`) для не-UUID возвращает хвост после последнего дефиса — короткий slugId. 2. `apps/client/src/features/ai-chat/services/ai-chat-service.ts:49-53` — `getBoundChat(pageId)` → `POST /ai-chat/bound-chat { pageId: <slugId> }`. 3. `apps/server/src/core/ai-chat/dto/ai-chat.dto.ts:41-44` — `BoundChatDto.pageId` валидируется только как `@IsString()`, без UUID-проверки, поэтому slugId проходит. 4. `apps/server/src/core/ai-chat/ai-chat.controller.ts:79-91` — `boundChat` → `aiChatRepo.findLatestByPage(user.id, workspace.id, dto.pageId)`. 5. `apps/server/src/database/repos/ai-chat/ai-chat.repo.ts:91` — `.where('pageId', '=', pageId)` по UUID-колонке `pageId` → Postgres `22P02` → 500. ## Impact - **Функция молча не работает.** Клиент ловит ошибку fail-soft (`use-open-ai-chat.ts:46-48`, `catch { resolved = null }`), поэтому при открытии страницы вместо «последнего чата этой страницы» открывается новый. Пользователь тихо теряет привязку чата к документу. - **500 + стек в логах на каждое открытие страницы** со slug-URL (а это все обычные ссылки на страницы) — постоянный шум уровня ERR. Severity: medium (не крашит UX, но фича сломана и логи засоряются 500-ками). ## Фикс Резолвить входящий id в реальный UUID страницы **на сервере**, т.к. `PageRepo.findById` (`apps/server/src/database/repos/page/page.repo.ts:65`, ветка `isValidUUID` на стр. 117-121) **уже принимает и UUID, и slugId**: ```ts // ai-chat.controller.ts, boundChat(...) const page = await this.pageRepo.findById(dto.pageId); // принимает slugId ИЛИ uuid if (!page || page.workspaceId !== workspace.id) return { chatId: null }; const chat = await this.aiChatRepo.findLatestByPage(user.id, workspace.id, page.id); // page.id — настоящий uuid return { chatId: chat?.id ?? null }; ``` Дополнительно: - Переименовать на клиенте `pageId` → `slugId` в `use-open-ai-chat.ts`/`getBoundChat`, чтобы убрать семантическую путаницу (в поле лежит slugId). - Только `@IsUUID()` в DTO — **недостаточно**: это лишь превратит 500 в 400, но привязку чата всё равно сломает (клиент шлёт slugId). Нужен резолв slug→id. ## Acceptance / DoD - `POST /ai-chat/bound-chat` со slug-URL возвращает `200 { chatId }` и корректно находит последний чат страницы; 500/`22P02` в логах нет. - Открытие страницы, где ранее был чат, **переоткрывает именно его**, а не создаёт новый. - Тест на резолв slugId→pageId в `bound-chat` (и на отсутствие/чужой воркспейс → `{ chatId: null }`, без утечки). ## Окружение - Версия: server@0.94.1 (docmost@0.94.1) - Эндпоинт: `POST /api/ai-chat/bound-chat` - Дата наблюдения: 2026-07-03 ~14:11 UTC - Клиент: Safari 17.6 / macOS
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#312