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
This commit is contained in:
Qubasa
2025-09-30 15:13:40 +02:00
parent 8ad9f99606
commit fdc4b5f769
6 changed files with 65 additions and 49 deletions

View File

@@ -1,5 +1,6 @@
import logging import logging
import threading import threading
import traceback
import uuid import uuid
from contextlib import ExitStack from contextlib import ExitStack
from dataclasses import dataclass from dataclasses import dataclass
@@ -43,10 +44,14 @@ class ApiBridge(Protocol):
from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415 from clan_app.middleware.base import MiddlewareContext # noqa: PLC0415
with ExitStack() as stack: with ExitStack() as stack:
# Capture the current call stack up to this point
original_stack = traceback.format_stack()
context = MiddlewareContext( context = MiddlewareContext(
request=request, request=request,
bridge=self, bridge=self,
exit_stack=stack, exit_stack=stack,
original_traceback=original_stack,
) )
# Process through middleware chain # Process through middleware chain
@@ -56,11 +61,23 @@ class ApiBridge(Protocol):
f"{middleware.__class__.__name__} => {request.method_name}", f"{middleware.__class__.__name__} => {request.method_name}",
) )
middleware.process(context) 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 # 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( self.send_api_error_response(
request.op_key or "unknown", request.op_key or "unknown",
str(e), error_msg,
["middleware_error"], ["middleware_error"],
) )
return return

View File

@@ -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__

View File

@@ -5,7 +5,7 @@ from clan_lib.api import MethodRegistry, from_dict
from clan_app.api.api_bridge import BackendRequest from clan_app.api.api_bridge import BackendRequest
from .base import Middleware, MiddlewareContext from .base import Middleware, MiddlewareContext, MiddlewareError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -27,7 +27,6 @@ class ArgumentParsingMiddleware(Middleware):
reconciled_arguments[k] = from_dict(arg_class, v) reconciled_arguments[k] = from_dict(arg_class, v)
# Create a new request with reconciled arguments # Create a new request with reconciled arguments
updated_request = BackendRequest( updated_request = BackendRequest(
method_name=context.request.method_name, method_name=context.request.method_name,
args=reconciled_arguments, args=reconciled_arguments,
@@ -37,12 +36,12 @@ class ArgumentParsingMiddleware(Middleware):
context.request = updated_request context.request = updated_request
except Exception as e: except Exception as e:
log.exception( # Create enhanced exception with original calling context
f"Error while parsing arguments for {context.request.method_name}", 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", # Chain the exceptions to preserve both tracebacks
str(e), raise enhanced_error from e
["argument_parsing", context.request.method_name],
)
raise

View File

@@ -12,6 +12,25 @@ class MiddlewareContext:
request: "BackendRequest" request: "BackendRequest"
bridge: "ApiBridge" bridge: "ApiBridge"
exit_stack: ExitStack 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) @dataclass(frozen=True)

View File

@@ -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.custom_logger import RegisteredHandler, setup_logging
from clan_lib.log_manager import LogManager from clan_lib.log_manager import LogManager
from .base import Middleware, MiddlewareContext from .base import Middleware, MiddlewareContext, MiddlewareError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -43,15 +43,15 @@ class LoggingMiddleware(Middleware):
).get_file_path() ).get_file_path()
except Exception as e: except Exception as e:
log.exception( # Create enhanced exception with original calling context
f"Error while handling request header of {context.request.method_name}", 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", # Chain the exceptions to preserve both tracebacks
str(e), raise enhanced_error from e
["header_middleware", context.request.method_name],
)
return
# Register logging context manager # Register logging context manager
class LoggingContextManager: class LoggingContextManager:

View File

@@ -5,7 +5,7 @@ from clan_lib.api import MethodRegistry
from clan_app.api.api_bridge import BackendResponse from clan_app.api.api_bridge import BackendResponse
from .base import Middleware, MiddlewareContext from .base import Middleware, MiddlewareContext, MiddlewareError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -31,11 +31,12 @@ class MethodExecutionMiddleware(Middleware):
context.bridge.send_api_response(response) context.bridge.send_api_response(response)
except Exception as e: except Exception as e:
log.exception( # Create enhanced exception with original calling context
f"Error while handling result of {context.request.method_name}", enhanced_error = MiddlewareError(
) f"Error in method '{context.request.method_name}'",
context.bridge.send_api_error_response( context.original_traceback,
context.request.op_key or "unknown", e,
str(e),
["method_execution", context.request.method_name],
) )
# Chain the exceptions to preserve both tracebacks
raise enhanced_error from e