mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 14:55:08 +00:00
refactor(web): one error-response path for api_v3 (#624)
* refactor(web): answer unhandled api_v3 errors from one blueprint handler
Fifty-three api_v3 routes ended in a copy of the same catch-all: log the
traceback, return {status, "An error occurred; see logs for details",
details: describe_exception(e)} with a 500. They are replaced by one
errorhandler on the api_v3 blueprint that returns exactly that body.
It lives on the blueprint rather than falling through to app.py's global
handler because the two answers differ: the global one adds
error_code: UNKNOWN_ERROR, and api_client.js sends a body with an
error_code to the error modal and one without to a plain toast. A
blueprint handler also gives tests that mount api_v3 on a bare Flask app
the same answer the real app gives.
Only handlers that were byte-for-byte that shape were removed (matched on
the AST, and each rewritten function re-parsed and compared). Handlers
with their own message, extra keys, operation-history records or cleanup
stay, as does execute_plugin_action's step-1 handler, which sits inside
an `except subprocess.TimeoutExpired` arm that would otherwise turn a
plugin's timeout into a 408.
HTTPExceptions raised inside a route go back as themselves in the global
handler's 4xx shape. Where a removed catch-all used to swallow one (only
delete_plugin_asset's non-silent get_json() is reachable), a malformed
request now gets its 415/400 instead of a 500.
Most of the diff is re-indentation from unwrapping the try blocks;
`git diff -w` shows the real change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): plugin action errors name the real failure, not UnboundLocalError
execute_plugin_action bound a local `logger` in its JSON-parsing arm,
which made `logger` local to the whole function. Every other
`logger.error` in it then raised UnboundLocalError, so a failing OAuth
step-1 script was reported as "UnboundLocalError: cannot access local
variable 'logger'" -- from the step-1 handler, and before the previous
commit from the route's outer catch-all too. Use the module logger.
Found by comparing every api_v3 route's forced-failure response before
and after the catch-all consolidation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(web): drop the error category and exception-name code guessing
WebInterfaceError derived an ErrorCategory from every error code and put
it in each structured error body as `error_category`. Nothing reads it:
not the web UI (static/ and templates/), not the tests beyond the ones
pinning the mapping itself, and not any plugin in ledmatrix-plugins. The
enum, the inference table and the JSON key go.
from_exception() could also guess an error code from the exception's
class name ("Config" -> CONFIG_LOAD_FAILED, and so on). Every caller
passes a code, so the guess never ran; error_code is now required.
suggested_fixes stays: the error dialog in static/v3/js/utils/
error_handler.js lists them.
The REST reference loses error_category and says what an unanticipated
exception in an /api/v3 route answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(web): one call for the from_exception error responses
Nine plugin routes built a structured error by hand:
from src.web_interface.errors import WebInterfaceError
error = WebInterfaceError.from_exception(e, ErrorCode.X)
return error_response(error.error_code, error.message,
details=error.details, context=error.context,
status_code=500)
That is now exception_error_response(e, ErrorCode.X) in api_helpers, so
error_response() is the only structured-error entry point the routes
use. The three operation-history routes never passed the context, and
with_context=False keeps their bodies exactly as they were; a test
compares the helper against the hand-written pair for both forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): one api_v3 error-response path
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,13 +35,16 @@ from typing import Dict, Any, Optional, Tuple, Type
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
logger = logging.getLogger(__name__)
|
||||
# Import new infrastructure
|
||||
from src.web_interface.api_helpers import success_response, error_response, validate_request_json
|
||||
from src.web_interface.api_helpers import (success_response, error_response,
|
||||
exception_error_response, validate_request_json)
|
||||
from src.web_interface.errors import ErrorCode
|
||||
from src.web_interface.secret_helpers import (find_secret_fields, mask_all_secret_values,
|
||||
merge_secrets, remove_empty_secrets,
|
||||
separate_secrets,
|
||||
strip_masked_values)
|
||||
from src.web_interface.error_handler import describe_exception, redact_text
|
||||
from src.web_interface.error_handler import (describe_exception, http_exception_payload,
|
||||
redact_text, unhandled_exception_payload)
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from src.plugin_system.operation_types import OperationType
|
||||
from src.web_interface.validators import (
|
||||
validate_file_upload
|
||||
@@ -114,6 +117,43 @@ SYSTEM_FONTS = frozenset([
|
||||
'clr6x12', 'helvr12', 'texgyre-27'
|
||||
])
|
||||
api_v3 = Blueprint('api_v3', __name__)
|
||||
|
||||
|
||||
@api_v3.errorhandler(Exception)
|
||||
def _api_v3_unhandled_exception(error):
|
||||
"""The answer for any exception an api_v3 route does not handle itself.
|
||||
|
||||
Fifty-odd routes used to end in the same four lines -- log the traceback,
|
||||
return {status, message: "An error occurred; see logs for details",
|
||||
details: describe_exception(e)} with a 500. This is those four lines, once.
|
||||
A route still catches for itself when its failure needs something else: a
|
||||
specific message, extra keys, an operation-history record, or cleanup.
|
||||
|
||||
It is registered on the blueprint, not left to web_interface/app.py's
|
||||
global handler, because the two answers differ: the global one adds
|
||||
`error_code: UNKNOWN_ERROR`, and the plugin API client treats a body with
|
||||
an error_code differently from one without (see api_client.js). Tests that
|
||||
mount this blueprint on a bare Flask app get the same answer as the real
|
||||
app does, too.
|
||||
|
||||
`details` is describe_exception(), which redacts credentials and caps the
|
||||
length. CodeQL reads returning it as stack-trace exposure; it is the
|
||||
project's deliberate trade-off, because a device whose storage is failing
|
||||
otherwise answers "see logs for details" from the log viewer too
|
||||
(test_web_error_detail.py).
|
||||
|
||||
Werkzeug's HTTPExceptions subclass Exception, so a 400/405/413/415 raised
|
||||
inside a route lands here as well; it goes back as itself, in the global
|
||||
handler's shape. A 404 or explicit 500 never arrives: Flask prefers the
|
||||
app's code-specific handlers over a blueprint's class-based one.
|
||||
"""
|
||||
if isinstance(error, HTTPException):
|
||||
return jsonify(http_exception_payload(error)), error.code or 500
|
||||
logger.error("Unhandled exception in %s", request.endpoint or request.path,
|
||||
exc_info=error)
|
||||
return jsonify(unhandled_exception_payload(error)), 500
|
||||
|
||||
|
||||
def _get_plugin_version(plugin_id: str) -> str:
|
||||
"""Read the installed version from a plugin's manifest.json.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user