fix(annotations): answer a fixed coded message when the filtered image list fails
A failed prompt-link or image-tag read under a Tags, category or rating filter returned the raw exception text (SQL or database messages) in the 500 body. The workspace listing now answers code listing-failed with a fixed sentence and logs the traceback. The legacy output listing and the inspector image-tag read now log with exc_info as well.
This commit is contained in:
+25
-6
@@ -74,6 +74,7 @@ IMAGE_EXTENSIONS = _IMAGE_EXTENSIONS
|
||||
# The header records the card parses (see utils.card_seed.header_params).
|
||||
_CARD_RECORDS = ("prompt", "workflow")
|
||||
_PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
||||
_LISTING_FAILED = "The image list could not be filtered. Check server logs for details."
|
||||
|
||||
|
||||
def _card_header(path: Path) -> tuple:
|
||||
@@ -1173,7 +1174,7 @@ class ImageRoutesMixin:
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Output images error: {e}")
|
||||
self.logger.error(f"Output images error: {e}", exc_info=True)
|
||||
return web.json_response(
|
||||
{"success": False, "error": _safe_error(e)}, status=500
|
||||
)
|
||||
@@ -1217,11 +1218,27 @@ class ImageRoutesMixin:
|
||||
for index, root in enumerate(output_dirs)
|
||||
if root_index is None or index == root_index
|
||||
]
|
||||
page, total, has_more, next_cursor, positive_count = (
|
||||
await self._run_in_executor(
|
||||
self._select_workspace_page, listings, filters, after, limit, scope
|
||||
try:
|
||||
page, total, has_more, next_cursor, positive_count = (
|
||||
await self._run_in_executor(
|
||||
self._select_workspace_page, listings, filters, after, limit, scope
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
# A filter that needs the prompt links or the image tags cannot be
|
||||
# served without them. Never echo the exception: it can carry SQL
|
||||
# or database text. The traceback goes to the server log.
|
||||
self.logger.error(
|
||||
"Workspace image list could not be filtered", exc_info=True
|
||||
)
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"code": "listing-failed",
|
||||
"error": _LISTING_FAILED,
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
)
|
||||
images = await self._run_in_executor(self._format_workspace_page, page)
|
||||
result = {
|
||||
"success": True,
|
||||
@@ -2652,7 +2669,9 @@ class ImageRoutesMixin:
|
||||
try:
|
||||
found = self.db.current_image_tags({locator: (size, mtime_ns)})
|
||||
except Exception:
|
||||
self.logger.warning("Inspection image tags could not be read")
|
||||
self.logger.warning(
|
||||
"Inspection image tags could not be read", exc_info=True
|
||||
)
|
||||
return {"state": "unavailable", "tags": []}
|
||||
tags = found.get(locator)
|
||||
if tags is None:
|
||||
|
||||
@@ -1696,8 +1696,12 @@ class TestInspectorImageTags(ImageAPITestCase):
|
||||
with patch.object(
|
||||
self.api.db, "current_image_tags", side_effect=RuntimeError("boom")
|
||||
):
|
||||
with self.assertLogs("prompt_manager", level="WARNING"):
|
||||
with self.assertLogs("prompt_manager", level="WARNING") as logged:
|
||||
response, failed = await self.inspect("sub/tagged.png")
|
||||
self.assertTrue(
|
||||
any(r.exc_info and "image tags" in r.getMessage() for r in logged.records),
|
||||
logged.output,
|
||||
)
|
||||
self.assertEqual(response.status, 200, failed)
|
||||
self.assertEqual(failed.get("image_tags"), {"state": "unavailable", "tags": []})
|
||||
self.assertEqual(failed["prompts"], envelope["prompts"])
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess # nosec B404
|
||||
import sys
|
||||
import unittest
|
||||
@@ -1232,15 +1233,21 @@ class TestWorkspaceImages(ImageAPITestCase):
|
||||
{"q": "zebra", "category": "Night"},
|
||||
{"sort": "rating"},
|
||||
):
|
||||
leak = 'near "gi": syntax error FROM generation_images'
|
||||
with (
|
||||
self.subTest(failing=params),
|
||||
patch.object(
|
||||
db, "get_image_link_matches", side_effect=RuntimeError("gone")
|
||||
db, "get_image_link_matches", side_effect=RuntimeError(leak)
|
||||
),
|
||||
self.assertLogs("prompt_manager", level="ERROR") as logged,
|
||||
):
|
||||
response, data = await self.get(**params)
|
||||
self.assertEqual(response.status, 500, data)
|
||||
self.assertNotIn("images", data)
|
||||
self.assertEqual(data["code"], "listing-failed")
|
||||
self.assertNotIn(leak, json.dumps(data))
|
||||
self.assertNotIn("syntax error", json.dumps(data))
|
||||
self.assertTrue(any(r.exc_info for r in logged.records))
|
||||
|
||||
async def test_filename_search_survives_a_database_failure(self):
|
||||
for name, mtime in (("zebra-1.png", 300), ("zebra-2.png", 200)):
|
||||
@@ -1989,18 +1996,39 @@ class TestWorkspaceImageTags(ImageAPITestCase):
|
||||
tagged = self.media("unlinked_tagged.png", mtime=102)
|
||||
self.media("dunes_by_name.png", mtime=101)
|
||||
self.tag(tagged, ["dunes"])
|
||||
leak = "no such table: media_annotation_files"
|
||||
with patch.object(
|
||||
self.api.db, "iter_image_tag_records", side_effect=RuntimeError("boom")
|
||||
self.api.db,
|
||||
"iter_image_tag_records",
|
||||
side_effect=sqlite3.OperationalError(leak),
|
||||
):
|
||||
# As a failed link read does under a Tags filter.
|
||||
response, body = await self.get(tags="dunes")
|
||||
# As a failed link read does under a Tags filter: a fixed coded
|
||||
# sentence, never the exception or SQL text; the traceback is logged.
|
||||
with self.assertLogs("prompt_manager", level="ERROR") as failed:
|
||||
response, body = await self.get(tags="dunes")
|
||||
self.assertEqual((response.status, body["success"]), (500, False))
|
||||
self.assertEqual(body["code"], "listing-failed")
|
||||
self.assertNotIn(leak, body["error"])
|
||||
self.assertNotIn("media_annotation_files", json.dumps(body))
|
||||
self.assertTrue(any(r.exc_info for r in failed.records))
|
||||
with self.assertLogs("prompt_manager", level="WARNING") as logged:
|
||||
response, body = await self.get(q="dunes")
|
||||
self.assertEqual(response.status, 200, body)
|
||||
self.assertEqual(self.names(body), ["dunes_by_name.png"])
|
||||
self.assertIn("image tags", "\n".join(logged.output))
|
||||
|
||||
async def test_legacy_output_failure_logs_its_traceback(self):
|
||||
self.media("legacy.png", mtime=100)
|
||||
with (
|
||||
patch.object(
|
||||
self.api, "_collect_output_images", side_effect=RuntimeError("x")
|
||||
),
|
||||
self.assertLogs("prompt_manager", level="ERROR") as logged,
|
||||
):
|
||||
response = await self.client.get("/prompt_manager/images/output")
|
||||
self.assertEqual(response.status, 500)
|
||||
self.assertTrue(any(r.exc_info for r in logged.records), logged.output)
|
||||
|
||||
async def test_legacy_gallery_and_search_shapes_unchanged(self):
|
||||
path = self.media("legacy.png", mtime=100)
|
||||
self.media("other.png", mtime=99)
|
||||
|
||||
Reference in New Issue
Block a user