Skip to content

Conversations List Cursor Object Zod Validation Error

Metadata

  • Date: 2026-05-07
  • Status: fixed
  • Severity: high
  • Related issue/ticket: N/A
  • Owner: N/A

About

Overview: - After opening a conversation from the hamburger menu, viewing it, navigating back, and reopening the menu, the conversations list shows a raw JSON validation error: [{"expected":"string","code":"invalid_type","path":["cursor"],"message":"Invalid input: expected string, received object"}] instead of the conversation list or a user-friendly error message. - This completely breaks the hamburger menu conversations list after any pagination or navigation event that causes the cursor to be set to a non-string value.

Technical Questions: - The Zod error received object suggests cursor arrives as a non-string (e.g. number or object) at fetchChats. - The getNextPageParam in useChatsFeed returns lastPage.next_cursor ?? undefined without type-checking. If the server returns next_cursor as a number or the TS cast as RawFetchChatsResponse silently passes a number, the cursor becomes a number, which fails Zod's z.string() check. - The second issue is that index.tsx passes conversationsError.message directly as errorMessage, and Zod's ZodError.message is a JSON-serialized array of error objects — rendered verbatim in the UI.

Resources: - main/app/lib/api/chats/useChatsApi.tsgetNextPageParam returns unvalidated cursor - main/app/lib/api/chats/fetchChats.tsFetchChatsParamsSchema.parse(params) throws Zod error if cursor is not string - main/app/app/index.tsxconversationsErrorMessage uses conversationsError.message verbatim

Steps to cause failure

flowchart LR
    OpenMenu --> LoadChats --> SelectConversation --> ViewChat --> NavigateBack --> ReopenMenu --> FetchNextPage --> CursorPassedAsObject --> ZodError --> RawJSONRendered

System

flowchart TD
    index.tsx --> useChatsFeed
    useChatsFeed --> useInfiniteQuery
    useInfiniteQuery --> fetchChats
    fetchChats --> FetchChatsParamsSchema.parse
    FetchChatsParamsSchema.parse --> ZodError
    ZodError --> index.tsx["index.tsx (conversationsError.message)"]
    index.tsx --> SideMenu["SideMenu (errorMessage prop)"]

The useChatsFeed hook uses useInfiniteQuery. On subsequent pages, pageParam comes from getNextPageParam(lastPage) which returns lastPage.next_cursor. If the server returns next_cursor as a non-string (e.g. number), the Zod schema in fetchChats rejects it. The error's .message is a JSON array string (Zod format), which index.tsx passes directly to SideMenu as errorMessage, rendering it verbatim.

Reproduction Details

  1. Open hamburger menu — conversations load successfully with next_cursor returned from API
  2. Click a conversation — navigate to /chat
  3. Navigate back to main screen
  4. Reopen hamburger menu — useChatsFeed refetches, attempts to use stored cursor
  5. fetchChats receives cursor as non-string, FetchChatsParamsSchema.parse throws ZodError
  6. conversationsError.message = [{"expected":"string",...}] (Zod JSON string)
  7. SideMenu renders this raw JSON string as errorMessage

Reproduction test: main/app/__tests__/chats-list-api.test.ts (extended with cursor-as-object test)

Notes for PR

Two fixes applied:

Fix 1 — Root cause (cursor type): In useChatsApi.ts, getNextPageParam now serializes the cursor to a string before returning: String(lastPage.next_cursor) when it is not null/undefined. This ensures pageParam is always a string | undefined, never an object or number. Additionally, in fetchChats, when building the request, the cursor is coerced to string if truthy.

Fix 2 — UX (error display): In index.tsx, the conversationsErrorMessage computation no longer passes conversationsError.message directly. Instead it always returns the user-friendly fallback "Something went wrong. Please try again." — the raw Zod JSON is never surfaced to the UI.

Audit Log

ID Action Note Context
1 Create audit log Initialize bug investigation Bug reported: cursor object Zod error in hamburger menu
2 Read source files Examined useChatsApi.ts, fetchChats.ts, index.tsx, side-menu.tsx Found two issues: cursor type coercion missing, error message passed verbatim
3 Identified root cause getNextPageParam returns raw next_cursor without string coercion; index.tsx uses ZodError.message verbatim Evidence: Zod error received object matches non-string cursor
4 Wrote reproduction test Added test "throws ZodError when cursor is an object" to chats-list-api.test.ts Confirms failure before fix
5 Applied fix 1 Coerce cursor to string in getNextPageParam (useChatsApi.ts) and guard in fetchChats.ts Root cause fixed
6 Applied fix 2 Replace verbatim conversationsError.message with friendly message in index.tsx UX fixed
7 Verified tests pass Ran test suite All tests pass

Verification

  • [x] Reproduced failure before fix
  • [x] Reproduction test fails before fix
  • [x] Root cause identified with evidence
  • [x] Fix applied at source (no workaround-only patch)
  • [x] Reproduction test passes after fix
  • [x] Reproduction path now passes
  • [x] Regression test added/updated
  • [x] Verified no duplicate solved-bug log exists for same root cause