3 Commits
Author SHA1 Message Date
Juan Denis d018a5e979 feat(api): centralize typed errors and request correlation 2026-08-18 02:42:25 -04:00
Juan DenisandClaude Opus 5 b31f8c7c5f test(api): assert the 500 behaviour instead of Flask internals
test_internal_error_handler_is_registered read app.error_handler_spec, a Flask
internal whose shape has moved between releases, and proved only that something
was registered. A handler can be registered and still return HTML or log
nothing, so the test could not fail for either way the guarantee actually breaks.

Now driven through app.handle_exception(), Flask's own entry point for an
unhandled exception, asserting the response is JSON and that the request was
logged. Not done by registering a route that raises: the `app` fixture is
function-scoped but wraps a session-scoped Flask object, so the route would leak
into every later test and collide on re-registration. PROPAGATE_EXCEPTIONS is
forced off for the call because TESTING=True turns it on, which re-raises before
any handler runs.

The log assertion matches this handler's own wording rather than just the path.
Flask's log_exception() already emits "Exception on /path [GET]" before any
handler runs, so a path-only assertion passes even with the handler's logging
deleted — confirmed by mutation, which is how the first version of this test was
caught being vacuous. Both mutations (return HTML, drop the log) now fail, on
different assertions.

Raised by a reviewer on the PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 18:06:19 -04:00
Juan DenisandClaude Opus 5 153ba83fc6 feat(api): global error handlers and a real logging configuration
Tracing issue #101 turned up that a panel failing to serve its own agent
installer left no diagnostic trail at all. The backend had exactly one error
handler -- the 404/SPA one -- no 500 or HTTPException handler, and nothing
anywhere configured logging, while only 2 of 100 app/api modules ever call
logger.error.

Adds:

- an errorhandler(500) that logs the unhandled exception with its method and
  path and returns JSON. Flask routes here only when it is not propagating, so
  pytest and the dev server still re-raise with a full traceback.
- an errorhandler(HTTPException) so framework-generated errors under /api/
  (405 from the router, 413, abort()) return JSON instead of Werkzeug's HTML
  page to clients that are parsing JSON. Non-API paths keep the HTML page, and
  the more specific 404/500 handlers still win for those codes.
- _configure_logging(): a stderr handler with a timestamp/level/logger-name
  formatter, level from LOG_LEVEL. Services across the codebase log through
  logging.getLogger(__name__); with no root handler those records fell through
  to logging's last-resort handler -- WARNING and above only, unformatted, INFO
  dropped on the floor.

Two things the container run caught while writing this: Flask's default handler
has to come off app.logger or every record prints twice (once as %(module)s via
Flask's handler, once as %(name)s via ours, both reaching stderr through
propagation), and the level must not key off DEBUG alone -- TestingConfig sets
DEBUG=True, which would have moved the whole test suite to DEBUG logging as a
side effect. Both are pinned by tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 05:27:27 -04:00