{"record":{"id":"b4baa93ec8b4e3a2","repo":"langgenius/dify","slug":"invalid-cursor","errorCode":null,"errorMessage":"Invalid cursor","messagePattern":"Invalid cursor","errorType":"http","errorClass":"BadRequest","httpStatus":400,"severity":"warning","filePath":"api/controllers/console/explore/installed_app.py","lineNumber":79,"sourceCode":"        return None\n    return file_helpers.get_signed_file_url(icon)\n\n\ndef _encode_installed_app_cursor(cursor: InstalledAppCursor) -> str:\n    payload = cursor.model_dump_json().encode()\n    return base64.urlsafe_b64encode(payload).decode().rstrip(\"=\")\n\n\ndef _decode_installed_app_cursor(cursor: str | None) -> InstalledAppCursor | None:\n    if cursor is None:\n        return None\n\n    try:\n        padded_cursor = cursor + \"=\" * (-len(cursor) % 4)\n        payload = base64.b64decode(padded_cursor, altchars=b\"-_\", validate=True)\n        return InstalledAppCursor.model_validate_json(payload)\n    except (binascii.Error, UnicodeDecodeError, ValueError):\n        raise BadRequest(\"Invalid cursor\") from None\n\n\nclass InstalledAppInfoResponse(ResponseModel):\n    id: str\n    name: str\n    description: str\n    mode: AppMode\n    icon_type: IconType | None\n    icon: str | None\n    icon_background: str | None\n    use_icon_as_answer_icon: bool\n\n    @computed_field(return_type=str | None)  # type: ignore[prop-decorator]\n    @property\n    def icon_url(self) -> str | None:\n        return _build_icon_url(self.icon_type, self.icon)\n\n","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/controllers/console/explore/installed_app.py#L61-L97","documentation":"BadRequest (HTTP 400, werkzeug) with message 'Invalid cursor' is raised in _decode_installed_app_cursor when the base64url cursor query parameter cannot be decoded or validated as an InstalledAppCursor. The cursor is an opaque, base64url-encoded JSON blob produced by _encode_installed_app_cursor; any tampering, truncation, or format mismatch triggers this error.","triggerScenarios":"GET /console/explore/installed-apps?cursor=<malformed> where the cursor value is not valid base64url, has been truncated or manually edited, decodes to non-UTF-8 bytes, or fails Pydantic validation against the InstalledAppCursor schema.","commonSituations":"The cursor was truncated by a URL shortener or log truncation; the cursor was manually constructed or edited; a client bug stored a truncated or URL-decoded version of the cursor; the cursor was passed through a system that mangled the base64url characters (- and _).","solutions":["Restart pagination by omitting the cursor parameter entirely — this fetches the first page and returns a fresh next_cursor.","Ensure the client passes the next_cursor value from the API response verbatim, without URL-decoding, truncation, or modification.","If the cursor is being logged or displayed, ensure it is not truncated by tooling before being reused."],"exampleFix":"// before: cursor truncated or mangled\nGET /installed-apps?cursor=eyJsaW1pdCI6Mj\n// after: restart pagination without cursor, then use the returned next_cursor verbatim\nGET /installed-apps?limit=20\n// then: GET /installed-apps?cursor=<exact next_cursor from response>","handlingStrategy":"validation","validationCode":"function isValidCursor(cursor) {\n  if (!cursor) return true; // null/undefined is valid (first page)\n  try {\n    const padded = cursor + '='.repeat((4 - cursor.length % 4) % 4);\n    const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));\n    JSON.parse(decoded); // must be valid JSON\n    return true;\n  } catch {\n    return false;\n  }\n}\n// before calling the list endpoint:\nif (!isValidCursor(cursor)) cursor = null; // restart from page 1","typeGuard":"function isValidInstalledAppCursor(cursor) {\n  if (!cursor) return true;\n  try {\n    const padded = cursor + '='.repeat((4 - cursor.length % 4) % 4);\n    const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));\n    const parsed = JSON.parse(decoded);\n    return typeof parsed === 'object' && parsed !== null;\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":"try {\n  const page = await listInstalledApps(cursor);\n} catch (e) {\n  if (e.status === 400 && e.message?.includes('Invalid cursor')) {\n    // cursor is corrupt — restart pagination from the first page\n    const freshPage = await listInstalledApps(null);\n    return freshPage;\n  }\n  throw e;\n}","preventionTips":["Always pass the next_cursor from the API response verbatim — never truncate, URL-decode, or modify it.","On an 'Invalid cursor' error, restart pagination by omitting the cursor parameter.","If cursors are logged or displayed, ensure they are not truncated before reuse."],"tags":["pagination","cursor","bad-request","base64","installed-app"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}