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:
@@ -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
|
||||
|
||||
@@ -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__
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user