{"record":{"id":"af6b0fe43736c4bd","repo":"odysseus-dev/odysseus","slug":"angle-must-be-90-90-180-or-270","errorCode":null,"errorMessage":"Angle must be 90, -90, 180, or 270","messagePattern":"Angle must be 90, -90, 180, or 270","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/gallery/gallery_routes.py","lineNumber":517,"sourceCode":"        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.\n            with Image.open(img_path) as pil:\n                rotated = pil.rotate(-angle, expand=True)","sourceCodeStart":499,"sourceCodeEnd":535,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/gallery/gallery_routes.py#L499-L535","documentation":"HTTP 400 raised by POST /api/gallery/{image_id}/rotate when the angle parses as an int but is not one of the allowed set (90, -90, 180, 270). Note the docstring mentions only ±90/180 but the code also accepts 270; anything else (0, 45, 360, 90.5) is rejected after parsing.","triggerScenarios":"Sending {\"angle\": 45}, {\"angle\": 0}, {\"angle\": 360}, or a float like 90.5 (int() truncates floats, so 90.5 becomes 90 and passes — but 45.5 becomes 45 and fails).","commonSituations":"Free-form rotation controls; users expecting arbitrary angles; off-by-one or sign mistakes (e.g. -180 vs 180 passes, 270 vs -90 confusion).","solutions":["Clamp the UI to the four allowed values (90, -90, 180, 270).","Map arbitrary angles to repeated quarter turns client-side (e.g. 360 = no-op, skip the call).","Remember the API convention is clockwise; use -90 for counter-clockwise."],"exampleFix":"// before\nbody: JSON.stringify({angle: userChosenAngle}) // 45 -> 400\n// after\nconst ALLOWED = new Set([90, -90, 180, 270]);\nif (!ALLOWED.has(angle)) angle = Math.round(angle / 90) * 90;\nif (angle % 360 === 0) return; // nothing to do\nbody: JSON.stringify({angle});","handlingStrategy":"validation","validationCode":"const ALLOWED = [90, -90, 180, 270];\nif (!ALLOWED.includes(angle)) return showError('Angle must be 90, -90, 180, or 270');\nawait post(`/api/gallery/${id}/rotate`, {angle});","typeGuard":"const isAllowedAngle = (a) => [90, -90, 180, 270].includes(a);","tryCatchPattern":null,"preventionTips":["Use quarter-turn buttons instead of free-form sliders","Remember clockwise convention (-90 = CCW)","Skip the call entirely for net-360 rotations"],"tags":["http","validation","gallery","rotate","allowed-values"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}