diff --git a/py/lora_manager.py b/py/lora_manager.py index 1f66ddb4..88029eb8 100644 --- a/py/lora_manager.py +++ b/py/lora_manager.py @@ -33,6 +33,7 @@ from .utils.example_images_migration import ExampleImagesMigration from .services.websocket_manager import ws_manager from .services.example_images_cleanup_service import ExampleImagesCleanupService from .middleware.csp_middleware import relax_csp_for_remote_media +from .middleware.error_middleware import api_json_error logger = logging.getLogger(__name__) @@ -76,6 +77,11 @@ class LoraManager: """Initialize and register all routes using the new refactored architecture""" app = PromptServer.instance.app + # Register JSON error middleware for /api/* routes as the outermost + # middleware so it catches errors from all other middlewares. + if api_json_error not in app.middlewares: + app.middlewares.insert(0, api_json_error) + if relax_csp_for_remote_media not in app.middlewares: # Ensure CSP relaxer executes after ComfyUI's block_external_middleware so it can # see and extend the restrictive header instead of being overwritten by it. diff --git a/py/middleware/error_middleware.py b/py/middleware/error_middleware.py new file mode 100644 index 00000000..cea0332f --- /dev/null +++ b/py/middleware/error_middleware.py @@ -0,0 +1,71 @@ +"""JSON error middleware for API routes. + +Ensures all responses to /api/* requests return valid JSON that the +browser-extension frontend can JSON.parse() without crashing, even when +the route does not exist (404) or the handler raises an exception (500). + +Extension consumers call response.json() unconditionally — an HTML error +page causes ``SyntaxError: unexpected end of data`` that leaks into the +popup UI as a toast notification. +""" + +from __future__ import annotations + +import logging +from typing import Awaitable, Callable + +from aiohttp import web + +logger = logging.getLogger(__name__) + + +@web.middleware +async def api_json_error( + request: web.Request, + handler: Callable[[web.Request], Awaitable[web.Response]], +) -> web.Response: + """Return JSON ``{"success": false, "error": "..."}`` for API errors. + + Only intercepts paths starting with ``/api/`` — all other routes + (frontend pages, static files, WebSocket upgrades) pass through + unchanged. + """ + if not request.path.startswith("/api/"): + return await handler(request) + + try: + response = await handler(request) + return response + except web.HTTPException as exc: + # Let redirects (301, 302, 307, 308) propagate — they are not errors. + if exc.status < 400: + raise + + logger.warning( + "API %s %s returned HTTP %d: %s", + request.method, + request.path, + exc.status, + exc.reason, + ) + + return web.json_response( + {"success": False, "error": f"{exc.status}: {exc.reason}"}, + status=exc.status, + ) + except Exception as exc: + logger.error( + "API %s %s raised unhandled exception: %s", + request.method, + request.path, + exc, + exc_info=True, + ) + + return web.json_response( + { + "success": False, + "error": f"500: Internal Server Error ({type(exc).__name__})", + }, + status=500, + ) diff --git a/standalone.py b/standalone.py index c03645df..298d057f 100644 --- a/standalone.py +++ b/standalone.py @@ -2,6 +2,7 @@ import os import sys import json from py.middleware.cache_middleware import cache_control +from py.middleware.error_middleware import api_json_error from py.utils.settings_paths import ensure_settings_file # Set environment variable to indicate standalone mode @@ -157,7 +158,7 @@ class StandaloneServer: def __init__(self): self.app = web.Application( logger=logger, - middlewares=[cache_control], + middlewares=[api_json_error, cache_control], client_max_size=256 * 1024 * 1024, handler_args={ "max_field_size": HEADER_SIZE_LIMIT,