langgenius/dify · warning · BadRequest
Invalid cursor
Error message
Invalid cursor
What it means
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.
Source
Thrown at api/controllers/console/explore/installed_app.py:79
return None
return file_helpers.get_signed_file_url(icon)
def _encode_installed_app_cursor(cursor: InstalledAppCursor) -> str:
payload = cursor.model_dump_json().encode()
return base64.urlsafe_b64encode(payload).decode().rstrip("=")
def _decode_installed_app_cursor(cursor: str | None) -> InstalledAppCursor | None:
if cursor is None:
return None
try:
padded_cursor = cursor + "=" * (-len(cursor) % 4)
payload = base64.b64decode(padded_cursor, altchars=b"-_", validate=True)
return InstalledAppCursor.model_validate_json(payload)
except (binascii.Error, UnicodeDecodeError, ValueError):
raise BadRequest("Invalid cursor") from None
class InstalledAppInfoResponse(ResponseModel):
id: str
name: str
description: str
mode: AppMode
icon_type: IconType | None
icon: str | None
icon_background: str | None
use_icon_as_answer_icon: bool
@computed_field(return_type=str | None) # type: ignore[prop-decorator]
@property
def icon_url(self) -> str | None:
return _build_icon_url(self.icon_type, self.icon)
View on GitHub (pinned to ef8544b173)
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.
Example fix
// before: cursor truncated or mangled GET /installed-apps?cursor=eyJsaW1pdCI6Mj // after: restart pagination without cursor, then use the returned next_cursor verbatim GET /installed-apps?limit=20 // then: GET /installed-apps?cursor=<exact next_cursor from response>
Defensive patterns
Strategy: validation
Validate before calling
function isValidCursor(cursor) {
if (!cursor) return true; // null/undefined is valid (first page)
try {
const padded = cursor + '='.repeat((4 - cursor.length % 4) % 4);
const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
JSON.parse(decoded); // must be valid JSON
return true;
} catch {
return false;
}
}
// before calling the list endpoint:
if (!isValidCursor(cursor)) cursor = null; // restart from page 1 Type guard
function isValidInstalledAppCursor(cursor) {
if (!cursor) return true;
try {
const padded = cursor + '='.repeat((4 - cursor.length % 4) % 4);
const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
const parsed = JSON.parse(decoded);
return typeof parsed === 'object' && parsed !== null;
} catch {
return false;
}
} Try / catch
try {
const page = await listInstalledApps(cursor);
} catch (e) {
if (e.status === 400 && e.message?.includes('Invalid cursor')) {
// cursor is corrupt — restart pagination from the first page
const freshPage = await listInstalledApps(null);
return freshPage;
}
throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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 _).
Related errors
- Last Conversation Not Exists.
- You can't uninstall an app owned by the current tenant
- ${name} must be an integer when set
- ${name} exceeds maximum size of ${MAX_LIST_LENGTH} items
- Parameter '${key}' exceeds maximum size of ${MAX_LIST_LENGTH
AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12).
Data as JSON: /api/errors/b4baa93ec8b4e3a2.
Report an issue: GitHub.