From e67a02916ce100546ecd346f5b80bd0f92f75c28 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 26 Jan 2022 10:59:10 +0100 Subject: [PATCH 1/2] Add `headers` parameter to `HTTPException` --- starlette/exceptions.py | 11 ++++++++--- tests/test_exceptions.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/starlette/exceptions.py b/starlette/exceptions.py index fcb0776cc..8f28b6e2d 100644 --- a/starlette/exceptions.py +++ b/starlette/exceptions.py @@ -9,11 +9,14 @@ class HTTPException(Exception): - def __init__(self, status_code: int, detail: str = None) -> None: + def __init__( + self, status_code: int, detail: str = None, headers: dict = None + ) -> None: if detail is None: detail = http.HTTPStatus(status_code).phrase self.status_code = status_code self.detail = detail + self.headers = headers def __repr__(self) -> str: class_name = self.__class__.__name__ @@ -99,5 +102,7 @@ async def sender(message: Message) -> None: def http_exception(self, request: Request, exc: HTTPException) -> Response: if exc.status_code in {204, 304}: - return Response(status_code=exc.status_code) - return PlainTextResponse(exc.detail, status_code=exc.status_code) + return Response(status_code=exc.status_code, headers=exc.headers) + return PlainTextResponse( + exc.detail, status_code=exc.status_code, headers=exc.headers + ) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index cef03359f..80307a521 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -21,6 +21,10 @@ def not_modified(request): raise HTTPException(status_code=304) +def with_headers(request): + raise HTTPException(status_code=200, headers={"x-potato": "always"}) + + class HandledExcAfterResponse: async def __call__(self, scope, receive, send): response = PlainTextResponse("OK", status_code=200) @@ -34,6 +38,7 @@ async def __call__(self, scope, receive, send): Route("/not_acceptable", endpoint=not_acceptable), Route("/no_content", endpoint=no_content), Route("/not_modified", endpoint=not_modified), + Route("/with_headers", endpoint=with_headers), Route("/handled_exc_after_response", endpoint=HandledExcAfterResponse()), WebSocketRoute("/runtime_error", endpoint=raise_runtime_error), ] @@ -67,6 +72,12 @@ def test_not_modified(client): assert response.text == "" +def test_with_headers(client): + response = client.get("/with_headers") + assert response.status_code == 200 + assert response.headers["x-potato"] == "always" + + def test_websockets_should_raise(client): with pytest.raises(RuntimeError): with client.websocket_connect("/runtime_error"): From bb5ebeccd45ee1446ce1e1a9af3adfdcf82c5c78 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Wed, 26 Jan 2022 12:14:03 +0100 Subject: [PATCH 2/2] Update exceptions docs --- docs/exceptions.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/exceptions.md b/docs/exceptions.md index d083a2da7..bf460d229 100644 --- a/docs/exceptions.md +++ b/docs/exceptions.md @@ -4,6 +4,8 @@ how you return responses when errors or handled exceptions occur. ```python from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.requests import Request from starlette.responses import HTMLResponse @@ -11,10 +13,10 @@ HTML_404_PAGE = ... HTML_500_PAGE = ... -async def not_found(request, exc): +async def not_found(request: Request, exc: HTTPException): return HTMLResponse(content=HTML_404_PAGE, status_code=exc.status_code) -async def server_error(request, exc): +async def server_error(request: Request, exc: HTTPException): return HTMLResponse(content=HTML_500_PAGE, status_code=exc.status_code) @@ -40,7 +42,7 @@ In particular you might want to override how the built-in `HTTPException` class is handled. For example, to use JSON style responses: ```python -async def http_exception(request, exc): +async def http_exception(request: Request, exc: HTTPException): return JSONResponse({"detail": exc.detail}, status_code=exc.status_code) exception_handlers = { @@ -48,6 +50,18 @@ exception_handlers = { } ``` +The `HTTPException` is also equipped with the `headers` argument. Which allows the propagation +of the headers to the response class: + +```python +async def http_exception(request: Request, exc: HTTPException): + return JSONResponse( + {"detail": exc.detail}, + status_code=exc.status_code, + headers=exc.headers + ) +``` + ## Errors and handled exceptions It is important to differentiate between handled exceptions and errors. @@ -76,7 +90,7 @@ The `HTTPException` class provides a base class that you can use for any handled exceptions. The `ExceptionMiddleware` implementation defaults to returning plain-text HTTP responses for any `HTTPException`. -* `HTTPException(status_code, detail=None)` +* `HTTPException(status_code, detail=None, headers=None)` You should only raise `HTTPException` inside routing or endpoints. Middleware classes should instead just return appropriate responses directly.