{"record":{"id":"f3a2d646a0f82ad6","repo":"lfnovo/open-notebook","slug":"invalid-order-by-order-by-allowed-fields","errorCode":null,"errorMessage":"Invalid order_by: '{order_by}'. Allowed fields: {', '.join(sorted(allowed_fields))}. Allowed directions: asc, desc","messagePattern":"Invalid order_by: '(.+?)'\\. Allowed fields: (.+?)\\. Allowed directions: asc, desc","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"api/routers/notebooks.py","lineNumber":82,"sourceCode":"    order_by: str = Query(\"updated desc\", description=\"Order by field and direction\"),\n):\n    \"\"\"Get all notebooks with optional filtering and ordering.\"\"\"\n    try:\n        # Validate order_by against allowlist to prevent SurrealQL injection\n        allowed_fields = {\"name\", \"created\", \"updated\"}\n        allowed_directions = {\"asc\", \"desc\"}\n\n        parts = order_by.strip().lower().split()\n        if len(parts) == 1:\n            if parts[0] not in allowed_fields:\n                raise HTTPException(\n                    status_code=400,\n                    detail=f\"Invalid order_by field: '{order_by}'. Allowed fields: {', '.join(sorted(allowed_fields))}\",\n                )\n            validated_order_by = parts[0]\n        elif len(parts) == 2:\n            if parts[0] not in allowed_fields or parts[1] not in allowed_directions:\n                raise HTTPException(\n                    status_code=400,\n                    detail=f\"Invalid order_by: '{order_by}'. Allowed fields: {', '.join(sorted(allowed_fields))}. Allowed directions: asc, desc\",\n                )\n            validated_order_by = f\"{parts[0]} {parts[1]}\"\n        else:\n            raise HTTPException(\n                status_code=400,\n                detail=f\"Invalid order_by format: '{order_by}'. Expected 'field' or 'field direction'\",\n            )\n\n        # Build the query with counts\n        query = f\"\"\"\n            SELECT *,\n            count(<-reference.in) as source_count,\n            count(<-artifact.in) as note_count\n            FROM notebook\n            ORDER BY {validated_order_by}\n        \"\"\"","sourceCodeStart":64,"sourceCodeEnd":100,"githubUrl":"https://github.com/lfnovo/open-notebook/blob/a7de90d38aaf18ee85fd661854d35c11e44613e2/api/routers/notebooks.py#L64-L100","documentation":"400 from GET /api/v1/notebooks when order_by has two words but either the field is not allowed or the direction is not 'asc'/'desc'. Note the direction check is case-sensitive on the lowercased parts only insofar as the whole string was lowercased, so 'Created ASC' works but 'created ascending' or 'name descending' fails.","triggerScenarios":"GET /notebooks?order_by=created ascending, ?order_by=title desc, or ?order_by=name descending — wrong direction word or disallowed field with a direction.","commonSituations":"Sort UI sending full direction words; typos in direction; combining a bad field with a valid direction.","solutions":["Use exactly 'asc' or 'desc' (they're lowercased with the input, so case is safe) after an allowed field","Drop the direction to use the default sort","Validate order_by client-side against the allow-list before calling"],"exampleFix":"// before\nGET /api/v1/notebooks?order_by=updated descending\n// after\nGET /api/v1/notebooks?order_by=updated desc","handlingStrategy":"validation","validationCode":"const m = order_by.trim().toLowerCase().match(/^(name|created|updated)( (asc|desc))?$/);\nif (!m) order_by = 'created desc'; // normalize instead of erroring","typeGuard":"const isValidOrderBy2 = (s: string) =>\n  /^(name|created|updated)( asc| desc)?$/.test(s.trim().toLowerCase());","tryCatchPattern":"try { await api.getNotebooks({order_by: q}); } catch (e) { if (e.status === 400 && /Allowed directions/.test(e.detail)) refetchSorted('created desc'); }","preventionTips":["Use only asc/desc abbreviations","Map UI 'Ascending/Descending' labels to asc/desc before sending","Validate with a shared regex on both field and direction"],"tags":["notebooks","validation","http-400","query-params"],"backgroundTag":"invalid-query-parameter","analyzedSha":"a7de90d38aaf18ee85fd661854d35c11e44613e2","analyzedAt":"2026-08-27T02:39:58.166Z","schemaVersion":2},"datasetVersion":"2026-08-27T03:17:27.898Z"}