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
+28
View File
@@ -28,6 +28,29 @@ _IMAGE_EXTENSIONS = {
"image/webp": "webp",
}
# Sniffed from the first bytes, independent of the declared Content-Type: a
# text file uploaded as "image/png" passes the header check above and must
# still be caught before it reaches the worker as a bare 500 (#15). BMP and
# TIFF are included even though `_IMAGE_EXTENSIONS` does not accept them,
# because PIL - and so the worker - can decode both; this check only refuses
# what is not a real image at all, it does not narrow the allow-list above.
_IMAGE_SIGNATURES: tuple[tuple[bytes, ...], ...] = (
(b"\x89PNG\r\n\x1a\n",), # PNG
(b"\xff\xd8\xff",), # JPEG
(b"BM",), # BMP
(b"II*\x00", b"MM\x00*"), # TIFF, little- and big-endian
(b"GIF87a", b"GIF89a"), # GIF
)
def _looks_like_an_image(content: bytes) -> bool:
"""True if `content` starts with a known image format's magic bytes."""
for signatures in _IMAGE_SIGNATURES:
if any(content.startswith(sig) for sig in signatures):
return True
# WebP: "RIFF" + 4-byte size + "WEBP", the size in between is not fixed.
return len(content) >= 12 and content[:4] == b"RIFF" and content[8:12] == b"WEBP"
async def _task_or_404(db: AsyncSession, task_id: int) -> RecipeTask:
"""Shared by the endpoints of this router."""
@@ -60,6 +83,11 @@ async def _read_image(image: UploadFile) -> bytes:
f"{settings.max_upload_size_mb}MB"
),
)
if not _looks_like_an_image(content):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="the file's content does not match a supported image format",
)
return content