{"record":{"id":"2f7f07fa47f8bdb7","repo":"BerriAI/litellm","slug":"invalid-sort-order-must-be-asc-or-desc-2f7f07","errorCode":null,"errorMessage":"Invalid sort order. Must be 'asc' or 'desc'","messagePattern":"Invalid sort order\\. Must be 'asc' or 'desc'","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/management_endpoints/key_management_endpoints.py","lineNumber":5795,"sourceCode":"        return None\n    # Validate sort_by is a valid column\n    valid_columns: Final = [\n        \"spend\",\n        \"max_budget\",\n        \"created_at\",\n        \"updated_at\",\n        \"token\",\n        \"key_alias\",\n    ]\n    if sort_by not in valid_columns:\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": f\"Invalid sort column. Must be one of: {', '.join(valid_columns)}\"},\n        )\n\n    # Validate sort_order\n    if sort_order.lower() not in [\"asc\", \"desc\"]:\n        raise HTTPException(\n            status_code=400,\n            detail={\"error\": \"Invalid sort order. Must be 'asc' or 'desc'\"},\n        )\n\n    order_by[sort_by] = sort_order.lower()\n\n    return order_by\n\n\ndef _build_expires_where_clause(expires_filter: str, now: datetime) -> dict[str, object]:\n    if expires_filter == \"expired\":\n        return {\"AND\": [{\"expires\": {\"not\": None}}, {\"expires\": {\"lt\": now}}]}\n    return {\"OR\": [{\"expires\": None}, {\"expires\": {\"gte\": now}}]}\n\n\ndef _build_key_filter_conditions(\n    user_id: str | None,\n    team_id: str | None,","sourceCodeStart":5777,"sourceCodeEnd":5813,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/key_management_endpoints.py#L5777-L5813","documentation":"Raised by the LiteLLM proxy key-management endpoints when a list request supplies a sort_order value that is not 'asc' or 'desc'. The helper that builds the Prisma order_by clause validates the column first (sort_by) and then lower-cases sort_order and checks membership in ['asc','desc']; anything else gets an HTTP 400 before any query runs. It exists to prevent arbitrary strings from reaching the database layer.","triggerScenarios":"Calling GET /key/list (or any key-listing route that accepts sort_by/sort_order) with e.g. sort_order=ascending, sort_order=ASCENDING, sort_order='' (empty), sort_order='DESC ' (trailing whitespace), or a URL-mangled value like sort_order=desc%0A. Case-insensitive: 'ASC'/'Desc' pass because the check applies .lower() first.","commonSituations":"Frontend dropdowns that send 'ascending'/'descending' instead of 'asc'/'desc'; copy-pasted query strings from other APIs (e.g. Grafana-style 'ASC' is fine but '+desc' from unencoded spaces is not); typos like 'ascc' or 'dsc'; SDKs defaulting to 'descending' when no explicit value is set.","solutions":["Send exactly 'asc' or 'desc' (any letter casing) for sort_order, e.g. /key/list?sort_by=token&sort_order=desc","Fix the UI/SDK mapping so 'ascending'->'asc' and 'descending'->'desc' before the request leaves the client","URL-encode the query string properly and confirm no stray whitespace or newlines are appended","If the 400 mentions the column instead, also verify sort_by is one of the valid columns listed in the error detail (includes token, key_alias, updated_at, etc.)"],"exampleFix":"# before\ncurl 'http://localhost:4000/key/list?sort_by=token&sort_order=ascending'\n# after\ncurl 'http://localhost:4000/key/list?sort_by=token&sort_order=desc'   # 'ASC'/'Desc' also OK; server lower-cases","handlingStrategy":"validation","validationCode":"SORT_ORDERS = ('asc', 'desc')\n\ndef normalize_list_params(params: dict) -> dict:\n    if 'sort_order' in params:\n        so = str(params['sort_order']).strip().lower()\n        if so not in SORT_ORDERS:\n            raise ValueError(f\"sort_order must be one of {SORT_ORDERS}, got {params['sort_order']!r}\")\n        params['sort_order'] = so\n    return params","typeGuard":"def is_valid_sort_order(value: object) -> bool:\n    return isinstance(value, str) and value.strip().lower() in ('asc', 'desc')","tryCatchPattern":"try:\n    r = await client.get('/key/list', params=normalize_list_params(q))\n    r.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 400 and 'sort order' in e.response.text.lower():\n        q['sort_order'] = 'desc'          # correct and retry once\n        r = await client.get('/key/list', params=q)\n    else:\n        raise","preventionTips":["Map UI labels 'ascending'/'descending' to 'asc'/'desc' at the client boundary","Keep sort_order values in a shared constant/enum instead of free-form strings","Strip and lower-case the value before putting it in the URL"],"tags":["sort","query-params","validation","litellm-proxy","keys"],"backgroundTag":"invalid-query-parameter","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}