{"record":{"id":"c9fccbf0f1c11f2d","repo":"odysseus-dev/odysseus","slug":"invalid-angle","errorCode":null,"errorMessage":"Invalid angle","messagePattern":"Invalid angle","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/gallery/gallery_routes.py","lineNumber":515,"sourceCode":"            db.commit()\n            return {\"ok\": True, \"name\": new_name}\n        finally:\n            db.close()\n\n    # ---- POST /api/gallery/{image_id}/rotate ----\n    @router.post(\"/api/gallery/{image_id}/rotate\")\n    async def gallery_rotate(request: Request, image_id: str):\n        \"\"\"Rotate an image by ±90° or 180°. Updates the file on disk and the\n        width/height in the DB. Body: {angle: 90 | -90 | 180}.\"\"\"\n        from pathlib import Path\n        from PIL import Image\n        from io import BytesIO\n\n        data = await request.json()\n        try:\n            angle = int(data.get(\"angle\", 90))\n        except (TypeError, ValueError):\n            raise HTTPException(400, \"Invalid angle\")\n        if angle not in (90, -90, 180, 270):\n            raise HTTPException(400, \"Angle must be 90, -90, 180, or 270\")\n\n        user = get_current_user(request)\n        db = SessionLocal()\n        try:\n            img = db.query(GalleryImage).filter(GalleryImage.id == image_id).first()\n            if not img:\n                raise HTTPException(404, \"Image not found\")\n            if not user or img.owner != user:\n                raise HTTPException(403, \"Not your image\")\n\n            img_path = _gallery_image_path(img.filename)\n            if not img_path.exists():\n                raise HTTPException(404, \"Image file not found\")\n\n            # PIL rotates counter-clockwise; the API takes \"clockwise\"\n            # convention so we negate to match user expectation.","sourceCodeStart":497,"sourceCodeEnd":533,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/gallery/gallery_routes.py#L497-L533","documentation":"HTTP 400 raised by POST /api/gallery/{image_id}/rotate when the \"angle\" field cannot be converted to int. int(data.get('angle', 90)) raises TypeError (for non-numeric containers) or ValueError (for non-numeric strings like \"45deg\" or \"\"), and both are caught and mapped to this 400.","triggerScenarios":"Sending {\"angle\": \"sideways\"}, {\"angle\": \"\"}, {\"angle\": null}-adjacent values like a list, or omitting a valid default path by passing a dict/object.","commonSituations":"Frontend sending the angle as a formatted string; sliders that emit \"90°\"; API consumers assuming the field is optional-but-typed.","solutions":["Send angle as a JSON number: {\"angle\": 90}.","If building from UI state, coerce with Number() before serializing.","Strip unit suffixes before sending."],"exampleFix":"// before\nbody: JSON.stringify({angle: `${deg}°`})\n// after\nbody: JSON.stringify({angle: Number(deg)})","handlingStrategy":"type-guard","validationCode":"const angle = Number(rawAngle);\nif (!Number.isFinite(angle) || !Number.isInteger(angle)) return showError('Angle must be a number');","typeGuard":"const isParsableAngle = (v) => { const n = Number(v); return Number.isFinite(n); };","tryCatchPattern":null,"preventionTips":["Serialize angle as a JSON number","Coerce slider/string values with Number() first","Strip unit suffixes before sending"],"tags":["http","validation","gallery","rotate","type-coercion"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}