{"record":{"id":"f61aba1e5af1f741","repo":"multica-ai/multica","slug":"invalid-cursor","errorCode":null,"errorMessage":"invalid cursor","messagePattern":"invalid cursor","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"server/internal/handler/chat.go","lineNumber":1005,"sourceCode":"}\n\nfunc parseChatMessagesPageParams(r *http.Request) (int, pgtype.Timestamptz, pgtype.UUID, error) {\n\tlimit := 50\n\tif raw := r.URL.Query().Get(\"limit\"); raw != \"\" {\n\t\tparsed, err := strconv.Atoi(raw)\n\t\tif err != nil || parsed < 1 || parsed > 100 {\n\t\t\treturn 0, pgtype.Timestamptz{}, pgtype.UUID{}, errors.New(\"invalid limit\")\n\t\t}\n\t\tlimit = parsed\n\t}\n\n\trawBeforeCreatedAt := r.URL.Query().Get(\"before_created_at\")\n\trawBeforeID := r.URL.Query().Get(\"before_id\")\n\tif rawBeforeCreatedAt == \"\" && rawBeforeID == \"\" {\n\t\treturn limit, pgtype.Timestamptz{}, pgtype.UUID{}, nil\n\t}\n\tif rawBeforeCreatedAt == \"\" || rawBeforeID == \"\" {\n\t\treturn 0, pgtype.Timestamptz{}, pgtype.UUID{}, errors.New(\"invalid cursor\")\n\t}\n\tbeforeTime, err := time.Parse(time.RFC3339Nano, rawBeforeCreatedAt)\n\tif err != nil {\n\t\treturn 0, pgtype.Timestamptz{}, pgtype.UUID{}, errors.New(\"invalid cursor\")\n\t}\n\tbeforeID, err := util.ParseUUID(rawBeforeID)\n\tif err != nil {\n\t\treturn 0, pgtype.Timestamptz{}, pgtype.UUID{}, errors.New(\"invalid cursor\")\n\t}\n\treturn limit, pgtype.Timestamptz{Time: beforeTime, Valid: true}, beforeID, nil\n}\n\n// RegenerateChatQuickActionsResponse acknowledges an accepted refresh request.\n// message_id is the assistant turn the refreshed pills will attach to — the\n// client anchors its pending placeholder on it and resolves it when the\n// chat:quick_actions supplement arrives.\n// RegenerateChatQuickActionsRequest names the assistant turn the client is\n// refreshing. The server confirms it is still the session's latest turn before","sourceCodeStart":987,"sourceCodeEnd":1023,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/handler/chat.go#L987-L1023","documentation":"This error comes from parseChatMessagesPageParams when exactly one of the two cursor components is supplied: the endpoint requires `before_created_at` and `before_id` to be sent together as a pair for keyset (seek) pagination. Supplying only one half makes the cursor ambiguous — the server cannot do the (created_at, id) tuple comparison without both — so it rejects rather than guess. Both absent is fine (first page); both present is the only valid cursor form.","triggerScenarios":"GET with `?before_created_at=2024-01-01T00:00:00Z` but no `before_id`, or `?before_id=<uuid>` with no `before_created_at`. Typically caused by manually assembling cursor URLs or by a client that drops one field when serializing the next_cursor object.","commonSituations":"Custom client implementing pagination from API docs and sending only the timestamp; a serialization bug where an empty-string before_id is omitted by an HTTP library that skips empty params; bookmarking a truncated URL.","solutions":["Use the `next_cursor` object from the previous response verbatim — it contains both fields as a pair.","Send both `before_created_at` and `before_id`, or neither.","Audit client code that builds query strings conditionally and make the two fields emit together.","If implementing pagination from scratch, keyset-paginate on the (created_at, id) tuple returned in the page payload."],"exampleFix":"// before\nconst url = `/api/chat/${id}/messages?before_created_at=${encodeURIComponent(c.created_at)}`;\n\n// after\nconst url = `/api/chat/${id}/messages?before_created_at=${encodeURIComponent(c.created_at)}&before_id=${encodeURIComponent(c.id)}`;","handlingStrategy":"validation","validationCode":"function cursorParams(cursor) {\n  if (!cursor) return '';\n  const { created_at, id } = cursor ?? {};\n  const present = [created_at, id].filter(v => v !== undefined && v !== '').length;\n  if (present !== 0 && present !== 2) {\n    throw new TypeError('cursor must include both before_created_at and before_id, or neither');\n  }\n  return present === 2 ? `before_created_at=${encodeURIComponent(created_at)}&before_id=${encodeURIComponent(id)}` : '';\n}","typeGuard":"const isCompleteCursor = (c) => c == null || (typeof c?.created_at === 'string' && typeof c?.id === 'string' && c.created_at !== '' && c.id !== '');","tryCatchPattern":null,"preventionTips":["Persist the whole next_cursor object from the response; never rebuild it from parts.","Disable HTTP-client options that drop empty-string query params when composing cursor URLs.","Integration-test pagination by walking 3+ pages using only returned cursors."],"tags":["validation","pagination","cursor","http-400"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}