feat: implement timeout handling and error messages for image generation

This commit is contained in:
2026-05-31 11:54:11 +02:00
parent c410c2e80d
commit 5794c96c21
4 changed files with 85 additions and 26 deletions
+17
View File
@@ -0,0 +1,17 @@
## [Unreleased] - 2026-05-31
### Added
- Added frontend regression test for image-generation timeout handling.
### Changed
- Frontend API helper now supports per-call timeout overrides.
### Removed
- None.
### Fixed
- Fixed frontend image generation route to handle backend timeout/request errors gracefully instead of returning 500 tracebacks.
+2 -1
View File
@@ -12,9 +12,10 @@ def _backend(path: str) -> str:
def _api(method: str, path: str, *, token: str | None = None, **kwargs):
headers = kwargs.pop("headers", {})
timeout = kwargs.pop("timeout", 30)
if token:
headers["Authorization"] = f"Bearer {token}"
return httpx.request(method, _backend(path), headers=headers, timeout=30, **kwargs)
return httpx.request(method, _backend(path), headers=headers, timeout=timeout, **kwargs)
def _model_matches_modality(model: dict, modality: str) -> bool:
+44 -25
View File
@@ -1,4 +1,6 @@
"""Generate blueprint — text, image, video generation."""
import httpx
from flask import (
Blueprint, flash, jsonify, redirect, render_template, request, session, url_for,
)
@@ -8,6 +10,18 @@ from ..helpers import _api, _load_models, login_required
generate_bp = Blueprint("generate", __name__)
def _detail_or_default(resp, default: str) -> str:
try:
payload = resp.json()
except ValueError:
return default
if isinstance(payload, dict):
detail = payload.get("detail")
if detail:
return str(detail)
return default
@generate_bp.get("/generate")
@login_required
def index():
@@ -81,31 +95,36 @@ def image():
result = error = None
token = session["access_token"]
if request.method == "POST":
ref_file = request.files.get("reference_image")
if ref_file and ref_file.filename:
up_resp = _api(
"POST", "/images/upload",
token=token,
files={"file": (ref_file.filename,
ref_file.stream, ref_file.content_type)},
)
if up_resp.status_code not in (200, 201):
error = up_resp.json().get("detail", "Image upload failed.")
models = _load_models(token, "image")
return render_template("generate_image.html", result=result, error=error, models=models)
try:
ref_file = request.files.get("reference_image")
if ref_file and ref_file.filename:
up_resp = _api(
"POST", "/images/upload",
token=token,
files={"file": (ref_file.filename,
ref_file.stream, ref_file.content_type)},
)
if up_resp.status_code not in (200, 201):
error = _detail_or_default(up_resp, "Image upload failed.")
models = _load_models(token, "image")
return render_template("generate_image.html", result=result, error=error, models=models)
resp = _api("POST", "/generate/image", token=token, json={
"model": request.form.get("model", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"n": int(request.form.get("n", 1)),
"size": request.form.get("size", "1024x1024"),
"aspect_ratio": request.form.get("aspect_ratio", "").strip() or None,
"image_size": request.form.get("image_size", "").strip() or None,
})
if resp.status_code == 200:
result = resp.json()
else:
error = resp.json().get("detail", "Generation failed.")
resp = _api("POST", "/generate/image", token=token, timeout=120, json={
"model": request.form.get("model", "").strip(),
"prompt": request.form.get("prompt", "").strip(),
"n": int(request.form.get("n", 1)),
"size": request.form.get("size", "1024x1024"),
"aspect_ratio": request.form.get("aspect_ratio", "").strip() or None,
"image_size": request.form.get("image_size", "").strip() or None,
})
if resp.status_code == 200:
result = resp.json()
else:
error = _detail_or_default(resp, "Generation failed.")
except httpx.TimeoutException:
error = "Image generation timed out. Please try again."
except httpx.RequestError:
error = "Cannot reach generation service. Please try again."
models = _load_models(token, "image")
return render_template("generate_image.html", result=result, error=error, models=models)
@@ -184,4 +203,4 @@ def cancel_video_job(video_id: str):
"""Proxy cancel request to backend."""
resp = _api(
"POST", f"/generate/videos/{video_id}/cancel", token=session["access_token"])
return jsonify(resp.json()), resp.status_code
return jsonify(resp.json()), resp.status_code
+22
View File
@@ -3,6 +3,8 @@ import os
import pytest
from unittest.mock import MagicMock, patch
import httpx
os.environ.setdefault("FLASK_SECRET_KEY", "test-secret")
os.environ.setdefault("BACKEND_URL", "http://backend-mock")
@@ -311,6 +313,26 @@ def test_generate_image_success(client):
assert b"example.com/img.png" in resp.data
def test_generate_image_timeout_shows_message(client):
_set_auth(client)
def _request_side_effect(method, url, **kwargs):
if url.endswith("/generate/image"):
raise httpx.ReadTimeout("timed out")
if url.endswith("/auth/validate"):
return _mock_response(200, {"valid": True})
if url.endswith("/models/"):
return _mock_response(200, [{"id": "openai/dall-e-3", "name": "DALL-E", "modality": "image"}])
return _mock_response(200, {})
with patch("app.helpers.httpx.request", side_effect=_request_side_effect):
resp = client.post("/generate/image", data={
"model": "openai/dall-e-3", "prompt": "A cat", "n": "1", "size": "1024x1024"
})
assert resp.status_code == 200
assert b"timed out" in resp.data
def test_generate_video_page_renders(client):
_set_auth(client)
resp = client.get("/generate/video")