From 54a935cf1fc9e32a334ce8cd8086ba73c15270c5 Mon Sep 17 00:00:00 2001 From: mariatta Date: Mon, 19 Oct 2026 21:34:51 -0700 Subject: [PATCH] Keep the real status code on rendered error pages The error middleware rendered error.html for every HTTPException but returned it with status 200, so a 404 or 403 looked like a success to health probes, monitoring and Sentry. Pass the exception's status to the renderer, and let redirects raised as exceptions pass through. --- blurb_it/middleware.py | 12 ++++++-- tests/test_middleware.py | 59 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/test_middleware.py diff --git a/blurb_it/middleware.py b/blurb_it/middleware.py index e79d543..e04dea2 100644 --- a/blurb_it/middleware.py +++ b/blurb_it/middleware.py @@ -4,16 +4,24 @@ @web.middleware async def error_middleware(request, handler): - """Middleware to render error message using the template renderer.""" + """Middleware to render error message using the template renderer. + + Only client and server errors are rendered. Redirects and other + non-error responses raised as exceptions pass through untouched. + The rendered page keeps the original status code, so monitoring, + health probes and Sentry see the real error instead of a 200. + """ try: response = await handler(request) except web.HTTPException as ex: + if ex.status < 400: + raise if ex.text: message = ex.text else: message = ex.reason context = {"error_message": message, "status": ex.status} response = aiohttp_jinja2.render_template( - "error.html", request, context=context + "error.html", request, context=context, status=ex.status ) return response diff --git a/tests/test_middleware.py b/tests/test_middleware.py new file mode 100644 index 0000000..20e16cf --- /dev/null +++ b/tests/test_middleware.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import pathlib + +import aiohttp_jinja2 +import jinja2 +from aiohttp import web + +from blurb_it import middleware + + +def create_app() -> web.Application: + app = web.Application(middlewares=[middleware.error_middleware]) + aiohttp_jinja2.setup( + app, loader=jinja2.FileSystemLoader(pathlib.Path.cwd() / "templates") + ) + return app + + +async def test_not_found_keeps_status_code(aiohttp_client) -> None: + app = create_app() + client = await aiohttp_client(app) + + response = await client.get("/does-not-exist") + + assert response.status == 404 + text = await response.text() + assert "

404

" in text + assert "Not Found" in text + + +async def test_handler_error_renders_text_with_status(aiohttp_client) -> None: + async def forbidden(request): + raise web.HTTPForbidden(reason="Invalid CSRF token. Please retry.") + + app = create_app() + app.router.add_get("/forbidden", forbidden) + client = await aiohttp_client(app) + + response = await client.get("/forbidden") + + assert response.status == 403 + text = await response.text() + assert "

403

" in text + assert "Invalid CSRF token. Please retry." in text + + +async def test_redirect_passes_through(aiohttp_client) -> None: + async def redirect(request): + raise web.HTTPFound(location="/elsewhere") + + app = create_app() + app.router.add_get("/redirect", redirect) + client = await aiohttp_client(app) + + response = await client.get("/redirect", allow_redirects=False) + + assert response.status == 302 + assert response.headers["Location"] == "/elsewhere"