feat: implement timeout handling and error messages for image generation
This commit is contained in:
@@ -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.
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user