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.ts — getNextPageParam returns unvalidated cursor - main/app/lib/api/chats/fetchChats.ts — FetchChatsParamsSchema.parse(params) throws Zod error if cursor is not string - main/app/app/index.tsx — conversationsErrorMessage 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
- Open hamburger menu — conversations load successfully with
next_cursorreturned from API - Click a conversation — navigate to
/chat - Navigate back to main screen
- Reopen hamburger menu —
useChatsFeedrefetches, attempts to use stored cursor fetchChatsreceives cursor as non-string,FetchChatsParamsSchema.parsethrowsZodErrorconversationsError.message=[{"expected":"string",...}](Zod JSON string)SideMenurenders this raw JSON string aserrorMessage
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