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):
|
def _api(method: str, path: str, *, token: str | None = None, **kwargs):
|
||||||
headers = kwargs.pop("headers", {})
|
headers = kwargs.pop("headers", {})
|
||||||
|
timeout = kwargs.pop("timeout", 30)
|
||||||
if token:
|
if token:
|
||||||
headers["Authorization"] = f"Bearer {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:
|
def _model_matches_modality(model: dict, modality: str) -> bool:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
"""Generate blueprint — text, image, video generation."""
|
"""Generate blueprint — text, image, video generation."""
|
||||||
|
import httpx
|
||||||
|
|
||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, flash, jsonify, redirect, render_template, request, session, url_for,
|
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__)
|
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")
|
@generate_bp.get("/generate")
|
||||||
@login_required
|
@login_required
|
||||||
def index():
|
def index():
|
||||||
@@ -81,31 +95,36 @@ def image():
|
|||||||
result = error = None
|
result = error = None
|
||||||
token = session["access_token"]
|
token = session["access_token"]
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
ref_file = request.files.get("reference_image")
|
try:
|
||||||
if ref_file and ref_file.filename:
|
ref_file = request.files.get("reference_image")
|
||||||
up_resp = _api(
|
if ref_file and ref_file.filename:
|
||||||
"POST", "/images/upload",
|
up_resp = _api(
|
||||||
token=token,
|
"POST", "/images/upload",
|
||||||
files={"file": (ref_file.filename,
|
token=token,
|
||||||
ref_file.stream, ref_file.content_type)},
|
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.")
|
if up_resp.status_code not in (200, 201):
|
||||||
models = _load_models(token, "image")
|
error = _detail_or_default(up_resp, "Image upload failed.")
|
||||||
return render_template("generate_image.html", result=result, error=error, models=models)
|
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={
|
resp = _api("POST", "/generate/image", token=token, timeout=120, json={
|
||||||
"model": request.form.get("model", "").strip(),
|
"model": request.form.get("model", "").strip(),
|
||||||
"prompt": request.form.get("prompt", "").strip(),
|
"prompt": request.form.get("prompt", "").strip(),
|
||||||
"n": int(request.form.get("n", 1)),
|
"n": int(request.form.get("n", 1)),
|
||||||
"size": request.form.get("size", "1024x1024"),
|
"size": request.form.get("size", "1024x1024"),
|
||||||
"aspect_ratio": request.form.get("aspect_ratio", "").strip() or None,
|
"aspect_ratio": request.form.get("aspect_ratio", "").strip() or None,
|
||||||
"image_size": request.form.get("image_size", "").strip() or None,
|
"image_size": request.form.get("image_size", "").strip() or None,
|
||||||
})
|
})
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
else:
|
else:
|
||||||
error = resp.json().get("detail", "Generation failed.")
|
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")
|
models = _load_models(token, "image")
|
||||||
return render_template("generate_image.html", result=result, error=error, models=models)
|
return render_template("generate_image.html", result=result, error=error, models=models)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
os.environ.setdefault("FLASK_SECRET_KEY", "test-secret")
|
os.environ.setdefault("FLASK_SECRET_KEY", "test-secret")
|
||||||
os.environ.setdefault("BACKEND_URL", "http://backend-mock")
|
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
|
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):
|
def test_generate_video_page_renders(client):
|
||||||
_set_auth(client)
|
_set_auth(client)
|
||||||
resp = client.get("/generate/video")
|
resp = client.get("/generate/video")
|
||||||
|
|||||||
Reference in New Issue
Block a user