odysseus-dev/odysseus · warning · HTTPException

Angle must be 90, -90, 180, or 270

Error message

Angle must be 90, -90, 180, or 270

What it means

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.

Source

Thrown at routes/gallery/gallery_routes.py:517

        finally:
            db.close()

    # ---- POST /api/gallery/{image_id}/rotate ----
    @router.post("/api/gallery/{image_id}/rotate")
    async def gallery_rotate(request: Request, image_id: str):
        """Rotate an image by ±90° or 180°. Updates the file on disk and the
        width/height in the DB. Body: {angle: 90 | -90 | 180}."""
        from pathlib import Path
        from PIL import Image
        from io import BytesIO

        data = await request.json()
        try:
            angle = int(data.get("angle", 90))
        except (TypeError, ValueError):
            raise HTTPException(400, "Invalid angle")
        if angle not in (90, -90, 180, 270):
            raise HTTPException(400, "Angle must be 90, -90, 180, or 270")

        user = get_current_user(request)
        db = SessionLocal()
        try:
            img = db.query(GalleryImage).filter(GalleryImage.id == image_id).first()
            if not img:
                raise HTTPException(404, "Image not found")
            if not user or img.owner != user:
                raise HTTPException(403, "Not your image")

            img_path = _gallery_image_path(img.filename)
            if not img_path.exists():
                raise HTTPException(404, "Image file not found")

            # PIL rotates counter-clockwise; the API takes "clockwise"
            # convention so we negate to match user expectation.
            with Image.open(img_path) as pil:
                rotated = pil.rotate(-angle, expand=True)

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Clamp the UI to the four allowed values (90, -90, 180, 270).
  2. Map arbitrary angles to repeated quarter turns client-side (e.g. 360 = no-op, skip the call).
  3. Remember the API convention is clockwise; use -90 for counter-clockwise.

Example fix

// before
body: JSON.stringify({angle: userChosenAngle}) // 45 -> 400
// after
const ALLOWED = new Set([90, -90, 180, 270]);
if (!ALLOWED.has(angle)) angle = Math.round(angle / 90) * 90;
if (angle % 360 === 0) return; // nothing to do
body: JSON.stringify({angle});
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED = [90, -90, 180, 270];
if (!ALLOWED.includes(angle)) return showError('Angle must be 90, -90, 180, or 270');
await post(`/api/gallery/${id}/rotate`, {angle});

Type guard

const isAllowedAngle = (a) => [90, -90, 180, 270].includes(a);

Prevention

When it happens

Trigger: 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).

Common situations: Free-form rotation controls; users expecting arbitrary angles; off-by-one or sign mistakes (e.g. -180 vs 180 passes, 270 vs -90 confusion).

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/af6b0fe43736c4bd. Report an issue: GitHub.