From d2dd6b43bd6180797325fab06d8696b66e2e9f59 Mon Sep 17 00:00:00 2001 From: Sahariar Hasan Date: Mon, 19 Oct 2026 00:13:15 +0530 Subject: [PATCH 1/2] feat(dx): add @app.exception_handler decorator syntax Add decorator-based global exception handler registration while preserving the existing imperative API. Closes #274 --- oxyroute/app.py | 19 +++++++++++++++++++ tests/test_exception_handlers.py | 32 +++++++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/oxyroute/app.py b/oxyroute/app.py index 2e48ced..0a4710c 100644 --- a/oxyroute/app.py +++ b/oxyroute/app.py @@ -383,6 +383,25 @@ def add_exception_handler( """ self._app.add_exception_handler(exc_type, handler) + def exception_handler( + self, exc_type: type[BaseException] + ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + """ + Decorator to register a global exception handler for a specific exception type. + + Example:: + + @app.exception_handler(ValueError) + def handle_value_error(request, exc: ValueError): + return {"detail": str(exc)}, 400 + """ + + def decorator(handler: Callable[..., Any]) -> Callable[..., Any]: + self.add_exception_handler(exc_type, handler) + return handler + + return decorator + def set_middleware(self, handler: Callable[..., Any] | None) -> None: """ One optional pre-route callback ``(scope, protocol)`` — return ``None`` to pass through. diff --git a/tests/test_exception_handlers.py b/tests/test_exception_handlers.py index 69c6ed7..8167176 100644 --- a/tests/test_exception_handlers.py +++ b/tests/test_exception_handlers.py @@ -15,11 +15,6 @@ class SubCustomError(CustomError): app = App() - # Note: the `add_exception_handler` method might be used as a decorator or a regular method. - # We didn't implement it as a decorator returning the function, but in our `app.py` it's just: - # def add_exception_handler(self, exc_type: type[BaseException], handler: Callable[..., Any]) -> None: - # So we call it directly. - def handle_custom_error(scope, exc): return Response(status=400, body=exc.msg.encode()) @@ -56,3 +51,30 @@ async def _run() -> None: assert r3.status_code == 500 asyncio.run(_run()) + + +def test_exception_handler_decorator(): + """@app.exception_handler(exc_type) decorator syntax (issue #274).""" + + class DecoratorError(Exception): + def __init__(self, msg: str): + self.msg = msg + + app = App() + + @app.exception_handler(DecoratorError) + def handle_decorator_error(scope, exc: DecoratorError): + return Response(status=422, body=f"decorated:{exc.msg}".encode()) + + @app.get("/decorated-error") + def raise_decorated() -> str: + raise DecoratorError("via-decorator") + + async def _run() -> None: + transport = httpx.ASGITransport(app=asgi_test_app(app)) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + r = await c.get("/decorated-error") + assert r.status_code == 422 + assert r.text == "decorated:via-decorator" + + asyncio.run(_run()) From 115f214bf4887eb00927006f0742c92eacec1c0a Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 18 Oct 2026 22:26:29 +0300 Subject: [PATCH 2/2] fix(dx): preserve handler types and update docstring example --- oxyroute/app.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/oxyroute/app.py b/oxyroute/app.py index 0a4710c..f029f79 100644 --- a/oxyroute/app.py +++ b/oxyroute/app.py @@ -383,20 +383,18 @@ def add_exception_handler( """ self._app.add_exception_handler(exc_type, handler) - def exception_handler( - self, exc_type: type[BaseException] - ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def exception_handler(self, exc_type: type[BaseException]) -> Callable[[F], F]: """ Decorator to register a global exception handler for a specific exception type. Example:: @app.exception_handler(ValueError) - def handle_value_error(request, exc: ValueError): - return {"detail": str(exc)}, 400 + def handle_value_error(scope, exc: ValueError): + return Response(status=400, body=str(exc).encode()) """ - def decorator(handler: Callable[..., Any]) -> Callable[..., Any]: + def decorator(handler: F) -> F: self.add_exception_handler(exc_type, handler) return handler