From fdc4b5f7696ba855e1ed788c29a7aecf9db9befb Mon Sep 17 00:00:00 2001 From: Qubasa Date: Tue, 30 Sep 2025 15:13:40 +0200 Subject: [PATCH] clan-app: full context tracebacks If an exception now is thrown in one of the middlewares we will get a proper traceback instead of a cut off one like before --- pkgs/clan-app/clan_app/api/api_bridge.py | 21 +++++++++++++++++-- pkgs/clan-app/clan_app/api/middleware.py | 20 ------------------ .../clan_app/middleware/argument_parsing.py | 19 ++++++++--------- pkgs/clan-app/clan_app/middleware/base.py | 19 +++++++++++++++++ pkgs/clan-app/clan_app/middleware/logging.py | 18 ++++++++-------- .../clan_app/middleware/method_execution.py | 17 ++++++++------- 6 files changed, 65 insertions(+), 49 deletions(-) delete mode 100644 pkgs/clan-app/clan_app/api/middleware.py diff --git a/pkgs/clan-app/clan_app/api/api_bridge.py b/pkgs/clan-app/clan_app/api/api_bridge.py index 0d4569238..e6b040b85 100644 --- a/pkgs/clan-app/clan_app/api/api_bridge.py +++ b/pkgs/clan-app/clan_app/api/api_bridge.py @@ -1,5 +1,6 @@ import logging import threading +import traceback import uuid from contextlib import ExitStack from dataclasses import dataclass @@ -43,10 +44,14 @@ class ApiBridge(Protocol): from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415 with ExitStack() as stack: + # Capture the current call stack up to this point + original_stack = traceback.format_stack() + context = MiddlewareContext( request=request, bridge=self, exit_stack=stack, + original_traceback=original_stack, ) # Process through middleware chain @@ -56,11 +61,23 @@ class ApiBridge(Protocol): f"{middleware.__class__.__name__} => {request.method_name}", ) middleware.process(context) - except Exception as e: # noqa: BLE001 + except Exception as e: + from clan_app.middleware.base import ( # noqa: PLC0415 + MiddlewareError, + ) + # If middleware fails, handle error + log.exception(f"Middleware {middleware.__class__.__name__} failed") + + error_msg = str(e) + + if isinstance(e, MiddlewareError): + # If it's already a MiddlewareError, use it directly + error_msg = e.method_message + self.send_api_error_response( request.op_key or "unknown", - str(e), + error_msg, ["middleware_error"], ) return diff --git a/pkgs/clan-app/clan_app/api/middleware.py b/pkgs/clan-app/clan_app/api/middleware.py deleted file mode 100644 index 78cc138e0..000000000 --- a/pkgs/clan-app/clan_app/api/middleware.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Compatibility wrapper for relocated middleware components. - -This module preserves the legacy import path ``clan_app.api.middleware`` while -the actual middleware implementations now live in ``clan_app.middleware``. -""" - -from __future__ import annotations - -from warnings import warn - -import clan_app.middleware as _middleware -from clan_app.middleware import * # noqa: F403 - -warn( - "clan_app.api.middleware is deprecated; use clan_app.middleware instead", - DeprecationWarning, - stacklevel=2, -) - -__all__ = _middleware.__all__ diff --git a/pkgs/clan-app/clan_app/middleware/argument_parsing.py b/pkgs/clan-app/clan_app/middleware/argument_parsing.py index 8e580e843..61343a681 100644 --- a/pkgs/clan-app/clan_app/middleware/argument_parsing.py +++ b/pkgs/clan-app/clan_app/middleware/argument_parsing.py @@ -5,7 +5,7 @@ from clan_lib.api import MethodRegistry, from_dict from clan_app.api.api_bridge import BackendRequest -from .base import Middleware, MiddlewareContext +from .base import Middleware, MiddlewareContext, MiddlewareError log = logging.getLogger(__name__) @@ -27,7 +27,6 @@ class ArgumentParsingMiddleware(Middleware): reconciled_arguments[k] = from_dict(arg_class, v) # Create a new request with reconciled arguments - updated_request = BackendRequest( method_name=context.request.method_name, args=reconciled_arguments, @@ -37,12 +36,12 @@ class ArgumentParsingMiddleware(Middleware): context.request = updated_request except Exception as e: - log.exception( - f"Error while parsing arguments for {context.request.method_name}", + # Create enhanced exception with original calling context + enhanced_error = MiddlewareError( + f"Error in method '{context.request.method_name}'", + context.original_traceback, + e, ) - context.bridge.send_api_error_response( - context.request.op_key or "unknown", - str(e), - ["argument_parsing", context.request.method_name], - ) - raise + + # Chain the exceptions to preserve both tracebacks + raise enhanced_error from e diff --git a/pkgs/clan-app/clan_app/middleware/base.py b/pkgs/clan-app/clan_app/middleware/base.py index ae7cb0145..c9f5faeeb 100644 --- a/pkgs/clan-app/clan_app/middleware/base.py +++ b/pkgs/clan-app/clan_app/middleware/base.py @@ -12,6 +12,25 @@ class MiddlewareContext: request: "BackendRequest" bridge: "ApiBridge" exit_stack: ExitStack + original_traceback: list[str] + + +class MiddlewareError(Exception): + """Exception that preserves original calling context.""" + + def __init__( + self, message: str, original_frames: list[str], original_error: Exception + ) -> None: + # Store just the original error message for API responses + super().__init__(str(original_error)) + self.method_message = message + self.original_frames = original_frames + self.original_error = original_error + + def __str__(self) -> str: + # For traceback display, show in proper Python traceback order (oldest to newest) + original_context = "".join(self.original_frames) + return f"Traceback (most recent call last):\n{original_context.rstrip()}\nMethodExecutionError: {self.original_error}" @dataclass(frozen=True) diff --git a/pkgs/clan-app/clan_app/middleware/logging.py b/pkgs/clan-app/clan_app/middleware/logging.py index 09c4052b0..e356e8034 100644 --- a/pkgs/clan-app/clan_app/middleware/logging.py +++ b/pkgs/clan-app/clan_app/middleware/logging.py @@ -8,7 +8,7 @@ from clan_lib.async_run import AsyncContext, get_async_ctx, set_async_ctx from clan_lib.custom_logger import RegisteredHandler, setup_logging from clan_lib.log_manager import LogManager -from .base import Middleware, MiddlewareContext +from .base import Middleware, MiddlewareContext, MiddlewareError log = logging.getLogger(__name__) @@ -43,15 +43,15 @@ class LoggingMiddleware(Middleware): ).get_file_path() except Exception as e: - log.exception( - f"Error while handling request header of {context.request.method_name}", + # Create enhanced exception with original calling context + enhanced_error = MiddlewareError( + f"Error in method '{context.request.method_name}'", + context.original_traceback, + e, ) - context.bridge.send_api_error_response( - context.request.op_key or "unknown", - str(e), - ["header_middleware", context.request.method_name], - ) - return + + # Chain the exceptions to preserve both tracebacks + raise enhanced_error from e # Register logging context manager class LoggingContextManager: diff --git a/pkgs/clan-app/clan_app/middleware/method_execution.py b/pkgs/clan-app/clan_app/middleware/method_execution.py index e00964f6f..7d1c6f3a0 100644 --- a/pkgs/clan-app/clan_app/middleware/method_execution.py +++ b/pkgs/clan-app/clan_app/middleware/method_execution.py @@ -5,7 +5,7 @@ from clan_lib.api import MethodRegistry from clan_app.api.api_bridge import BackendResponse -from .base import Middleware, MiddlewareContext +from .base import Middleware, MiddlewareContext, MiddlewareError log = logging.getLogger(__name__) @@ -31,11 +31,12 @@ class MethodExecutionMiddleware(Middleware): context.bridge.send_api_response(response) except Exception as e: - log.exception( - f"Error while handling result of {context.request.method_name}", - ) - context.bridge.send_api_error_response( - context.request.op_key or "unknown", - str(e), - ["method_execution", context.request.method_name], + # Create enhanced exception with original calling context + enhanced_error = MiddlewareError( + f"Error in method '{context.request.method_name}'", + context.original_traceback, + e, ) + + # Chain the exceptions to preserve both tracebacks + raise enhanced_error from e