From ee4656adbf7b6f43fe30e4907ec7f35f9868b1fe Mon Sep 17 00:00:00 2001 From: Sanskar Jethi Date: Sun, 12 Apr 2026 00:35:34 +0100 Subject: [PATCH] feat: add headers support to HTTPException and auto-handle in router MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HTTPException now accepts an optional `headers` dict. When raised inside a route handler, the framework automatically catches it and returns the appropriate HTTP response — no manual `@app.exception` handler needed. Made-with: Cursor --- robyn/__init__.py | 2 ++ robyn/exceptions.py | 3 ++- robyn/router.py | 13 ++++++++++++ unit_tests/test_http_exception.py | 35 +++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 unit_tests/test_http_exception.py diff --git a/robyn/__init__.py b/robyn/__init__.py index 08dd0631d..ce2e52ced 100644 --- a/robyn/__init__.py +++ b/robyn/__init__.py @@ -12,6 +12,7 @@ from robyn.argument_parser import Config from robyn.authentication import AuthenticationHandler from robyn.dependency_injection import DependencyMap +from robyn.exceptions import HTTPException from robyn.env_populator import load_vars from robyn.events import Events from robyn.jsonify import jsonify @@ -811,4 +812,5 @@ def cors_middleware(request): "WebSocketDisconnect", "JsonBody", "MCPApp", + "HTTPException", ] diff --git a/robyn/exceptions.py b/robyn/exceptions.py index 5529c130f..f3e4f101e 100644 --- a/robyn/exceptions.py +++ b/robyn/exceptions.py @@ -2,11 +2,12 @@ class HTTPException(Exception): - def __init__(self, status_code: int, detail: str | None = None) -> None: + def __init__(self, status_code: int, detail: str | None = None, headers: dict | None = None) -> None: if detail is None: detail = http.HTTPStatus(status_code).phrase self.status_code = status_code self.detail = detail + self.headers = headers or {} def __str__(self) -> str: return f"{self.status_code}: {self.detail}" diff --git a/robyn/router.py b/robyn/router.py index 242e57531..0ed5187fc 100644 --- a/robyn/router.py +++ b/robyn/router.py @@ -6,6 +6,7 @@ from typing import Callable, Dict, List, NamedTuple, Optional, Union, is_typeddict from robyn import status_codes +from robyn.exceptions import HTTPException from robyn._param_utils import QueryParamValidationError, parse_route_param_names, resolve_individual_params from robyn.authentication import AuthenticationHandler, AuthenticationNotConfiguredError from robyn.dependency_injection import DependencyMap @@ -289,6 +290,12 @@ async def async_inner_handler(*args, **kwargs): headers=Headers({"Content-Type": "application/json"}), description=jsonify(err.error_detail), ) + except HTTPException as exc: + response = Response( + status_code=exc.status_code, + headers=Headers(exc.headers), + description=exc.detail or "", + ) except Exception as err: if exception_handler is None: raise @@ -315,6 +322,12 @@ def inner_handler(*args, **kwargs): headers=Headers({"Content-Type": "application/json"}), description=jsonify(err.error_detail), ) + except HTTPException as exc: + response = Response( + status_code=exc.status_code, + headers=Headers(exc.headers), + description=exc.detail or "", + ) except Exception as err: if exception_handler is None: raise diff --git a/unit_tests/test_http_exception.py b/unit_tests/test_http_exception.py new file mode 100644 index 000000000..2c3122ae6 --- /dev/null +++ b/unit_tests/test_http_exception.py @@ -0,0 +1,35 @@ +from robyn.exceptions import HTTPException + + +def test_http_exception_default(): + exc = HTTPException(404) + assert exc.status_code == 404 + assert exc.detail == "Not Found" + assert exc.headers == {} + + +def test_http_exception_custom_detail(): + exc = HTTPException(403, detail="Go away") + assert exc.detail == "Go away" + + +def test_http_exception_with_headers(): + exc = HTTPException( + 401, + detail="Token expired", + headers={"WWW-Authenticate": "Bearer"}, + ) + assert exc.status_code == 401 + assert exc.detail == "Token expired" + assert exc.headers == {"WWW-Authenticate": "Bearer"} + + +def test_http_exception_str(): + exc = HTTPException(500, detail="Internal error") + assert str(exc) == "500: Internal error" + + +def test_http_exception_repr(): + exc = HTTPException(400, detail="Bad input") + assert "HTTPException" in repr(exc) + assert "400" in repr(exc)