fix(vision): file non immagine e grafo malformato non escono più come 500 (#15)

Worker: Image.open().convert() in try/except (422 "image is not
decodable"); il grafo deve essere un dict (422 altrimenti); run_graph
distingue costruzione (GraphBuildError -> 422) da esecuzione (500 con
motivo). Router: verifica dei magic bytes (PNG/JPEG/BMP/TIFF/WebP/GIF)
prima di chiamare il worker.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 22:37:34 +02:00
parent 92f130479a
commit 9afe1e712b
5 changed files with 153 additions and 7 deletions
+25 -6
View File
@@ -10,9 +10,9 @@ import json
import numpy as np
from fastapi import FastAPI, File, Form, HTTPException, UploadFile, status
from PIL import Image
from PIL import Image, UnidentifiedImageError
from src.vision.runner import engine_version, run_graph
from src.vision.runner import GraphBuildError, engine_version, run_graph
app = FastAPI(title="TieMeasureFlow Vision Worker", version="0.1.0")
@@ -35,17 +35,36 @@ async def run(
detail=f"graph is not valid JSON: {exc}",
) from exc
frame = np.array(Image.open(image.file).convert("L"))
if not isinstance(parsed_graph, dict):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"graph must be a JSON object, got {type(parsed_graph).__name__}",
)
try:
frame = np.array(Image.open(image.file).convert("L"))
except (UnidentifiedImageError, OSError) as exc:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"image is not decodable: {exc}",
) from exc
try:
outcome = run_graph(frame, parsed_graph)
except ValueError as exc:
# from_dict refuses a schema version it does not handle; that is a bad
# request, not a server fault.
except GraphBuildError as exc:
# The graph itself is bad (schema version, a tool missing "kind", ...) -
# a bad request, not a server fault.
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(exc),
) from exc
except Exception as exc:
# The graph built fine; something failed while actually running it -
# an engine fault, not the caller's. Still named, not a naked 500.
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"vision run failed: {type(exc).__name__}: {exc}",
) from exc
return {
# `outcome.outputs` never carries NaN/inf/bool (see runner.run_graph):