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