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

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 .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

View File

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

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.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:

View File

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