{"record":{"id":"27b20e70d55d8c7d","repo":"multica-ai/multica","slug":"invalid-limit","errorCode":null,"errorMessage":"invalid limit","messagePattern":"invalid limit","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"server/internal/handler/chat.go","lineNumber":994,"sourceCode":"\ntype ChatMessagesCursorResponse struct {\n\tCreatedAt string `json:\"created_at\"`\n\tID        string `json:\"id\"`\n}\n\ntype ChatMessagesPageResponse struct {\n\tMessages   []ChatMessageResponse       `json:\"messages\"`\n\tLimit      int                         `json:\"limit\"`\n\tHasMore    bool                        `json:\"has_more\"`\n\tNextCursor *ChatMessagesCursorResponse `json:\"next_cursor,omitempty\"`\n}\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 {","sourceCodeStart":976,"sourceCodeEnd":1012,"githubUrl":"https://github.com/multica-ai/multica/blob/2c0912b6ec764b373d44eeea1e80f0d9f11ab417/server/internal/handler/chat.go#L976-L1012","documentation":"This error is returned by parseChatMessagesPageParams when the optional `limit` query parameter on the chat messages listing endpoint fails to parse as an integer, or parses to a value outside the allowed 1..100 window. The server defaults to 50 when the parameter is absent, so the error only fires on a malformed or out-of-range explicit value. It exists to cap page size and prevent non-numeric input from reaching the SQL query layer.","triggerScenarios":"GET /chat messages endpoint with `?limit=0`, `?limit=101`, `?limit=-5`, `?limit=abc`, or `?limit=50.5` (strconv.Atoi rejects floats). Any value that is not a base-10 integer in [1,100] triggers it; omitting limit entirely does not.","commonSituations":"Client UI letting users pick an unbounded 'load N messages' value; copy-pasting a cursor URL and hand-editing limit; a frontend sending limit as a float string after JSON serialization; automated scrapers probing with limit=1000.","solutions":["Clamp the requested limit client-side to the 1..100 range before building the URL (e.g. Math.min(Math.max(limit,1),100)).","Omit the limit parameter entirely when you want the default of 50.","Ensure the limit is serialized as an integer string, not a float or scientific-notation value.","Treat the 400 response as non-retryable: fix the input, do not backoff-and-retry."],"exampleFix":"// before\nconst url = `/api/chat/${id}/messages?limit=${userLimit}`;\n\n// after\nconst clamped = Math.min(Math.max(Number(userLimit) || 50, 1), 100);\nconst url = `/api/chat/${id}/messages?limit=${clamped}`;","handlingStrategy":"validation","validationCode":"function buildMessagesUrl(chatId, { limit, cursor } = {}) {\n  const params = new URLSearchParams();\n  if (limit !== undefined) {\n    const n = Number(limit);\n    if (!Number.isInteger(n) || n < 1 || n > 100) {\n      throw new RangeError(`limit must be an integer in [1,100], got ${limit}`);\n    }\n    params.set('limit', String(n));\n  }\n  if (cursor) {\n    if (!cursor.created_at || !cursor.id) throw new TypeError('cursor requires both created_at and id');\n    params.set('before_created_at', cursor.created_at);\n    params.set('before_id', cursor.id);\n  }\n  return `/api/chat/${chatId}/messages${params.size ? '?' + params : ''}`;\n}","typeGuard":"const isValidLimit = (v) => Number.isInteger(v) && v >= 1 && v <= 100;","tryCatchPattern":null,"preventionTips":["Centralize URL building for the messages endpoint in one helper so the clamp is applied everywhere.","Treat HTTP 400 from this endpoint as a programming error — log and fix the caller, never retry.","Keep the previous page's next_cursor opaque: store it verbatim instead of decomposing it."],"tags":["validation","pagination","query-params","http-400"],"backgroundTag":null,"analyzedSha":"2c0912b6ec764b373d44eeea1e80f0d9f11ab417","analyzedAt":"2026-08-15T13:25:18.241Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}