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
- 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.
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
- Use quarter-turn buttons instead of free-form sliders
- Remember clockwise convention (-90 = CCW)
- Skip the call entirely for net-360 rotations
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.