Updated route dispatch to properly handle aiohttp HTTP exceptions.
CI / Formatting (push) Successful in 21s
CI / Linting (push) Successful in 4s
CI / Tests (Python 3.12) (push) Successful in 2m53s
CI / Tests (Python 3.13) (push) Successful in 2m52s
CI / Tests (Python 3.14) (push) Successful in 2m42s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 5s
CI / Formatting (push) Successful in 21s
CI / Linting (push) Successful in 4s
CI / Tests (Python 3.12) (push) Successful in 2m53s
CI / Tests (Python 3.13) (push) Successful in 2m52s
CI / Tests (Python 3.14) (push) Successful in 2m42s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 5s
This commit is contained in:
+1
-1
Submodule docs updated: b4e73c6f16...d8cc18d242
@@ -161,7 +161,7 @@ async def editor_submit(ctx: RouteContext) -> web.Response:
|
|||||||
clip_url = ctx.routes.url_for(f"/view/{clip.id}")
|
clip_url = ctx.routes.url_for(f"/view/{clip.id}")
|
||||||
await ctx.owncast_client.send_message(f"{clip.creator} created a clip: {clip_url}")
|
await ctx.owncast_client.send_message(f"{clip.creator} created a clip: {clip_url}")
|
||||||
|
|
||||||
return web.HTTPFound(clip_url)
|
raise web.HTTPFound(clip_url)
|
||||||
|
|
||||||
|
|
||||||
@on_route("/view/{clip_id}", methods=["GET"])
|
@on_route("/view/{clip_id}", methods=["GET"])
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ async def create_submit(ctx: RouteContext) -> web.Response:
|
|||||||
|
|
||||||
# Redirect to the mod's voting page.
|
# Redirect to the mod's voting page.
|
||||||
vote_url = ctx.routes.url_for(f"/vote/{token}")
|
vote_url = ctx.routes.url_for(f"/vote/{token}")
|
||||||
return web.HTTPFound(vote_url)
|
raise web.HTTPFound(vote_url)
|
||||||
|
|
||||||
|
|
||||||
@on_route("/vote/{token}", methods=["GET"])
|
@on_route("/vote/{token}", methods=["GET"])
|
||||||
@@ -293,7 +293,7 @@ async def vote_submit(ctx: RouteContext) -> web.Response:
|
|||||||
|
|
||||||
# Redirect back to the voting page.
|
# Redirect back to the voting page.
|
||||||
vote_url = ctx.routes.url_for(f"/vote/{token}")
|
vote_url = ctx.routes.url_for(f"/vote/{token}")
|
||||||
return web.HTTPFound(vote_url)
|
raise web.HTTPFound(vote_url)
|
||||||
|
|
||||||
|
|
||||||
@on_route("/vote/{token}/events", streaming=True)
|
@on_route("/vote/{token}/events", streaming=True)
|
||||||
@@ -379,7 +379,7 @@ async def mod_end(ctx: RouteContext) -> web.Response:
|
|||||||
|
|
||||||
await manager.end()
|
await manager.end()
|
||||||
results_url = ctx.routes.url_for("/results")
|
results_url = ctx.routes.url_for("/results")
|
||||||
return web.HTTPFound(results_url)
|
raise web.HTTPFound(results_url)
|
||||||
|
|
||||||
|
|
||||||
@on_route("/vote/{token}/cancel", methods=["POST"])
|
@on_route("/vote/{token}/cancel", methods=["POST"])
|
||||||
|
|||||||
@@ -620,7 +620,8 @@ class RouteDispatcher:
|
|||||||
type(result).__name__,
|
type(result).__name__,
|
||||||
)
|
)
|
||||||
return web.Response(status=500)
|
return web.Response(status=500)
|
||||||
|
except web.HTTPException as exc:
|
||||||
|
return exc
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
mod_logger.warning(
|
mod_logger.warning(
|
||||||
"Route handler '%s' timed out after %ss.",
|
"Route handler '%s' timed out after %ss.",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ ModuleRoutes (ownership-scoped wrapper), and RouteContext.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import types
|
import types
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
@@ -511,6 +512,28 @@ class TestRouteDispatcherDispatch:
|
|||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
assert response.content_type == "application/json"
|
assert response.content_type == "application/json"
|
||||||
|
|
||||||
|
async def test_dispatch_dict_serialization_error_returns_500(
|
||||||
|
self, module_context: ModuleContext
|
||||||
|
) -> None:
|
||||||
|
"""JSON serialization errors return 500 instead of escaping dispatch."""
|
||||||
|
|
||||||
|
async def handler(ctx: RouteContext) -> dict[str, object]:
|
||||||
|
return {"bad": object()}
|
||||||
|
|
||||||
|
dispatcher = _make_dispatcher(module_context)
|
||||||
|
dispatcher.register("/api", handler, module_name="mod")
|
||||||
|
|
||||||
|
request = make_mocked_request("GET", "/owlbot/mod/api")
|
||||||
|
request.match_info["module_name"] = "mod"
|
||||||
|
request.match_info["path"] = "api"
|
||||||
|
|
||||||
|
response: web.StreamResponse | None = None
|
||||||
|
with contextlib.suppress(TypeError):
|
||||||
|
response = await dispatcher.dispatch(request)
|
||||||
|
|
||||||
|
assert response is not None
|
||||||
|
assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
|
||||||
async def test_dispatch_web_response(self, module_context: ModuleContext) -> None:
|
async def test_dispatch_web_response(self, module_context: ModuleContext) -> None:
|
||||||
"""Handler returning web.Response is passed through."""
|
"""Handler returning web.Response is passed through."""
|
||||||
|
|
||||||
@@ -527,6 +550,26 @@ class TestRouteDispatcherDispatch:
|
|||||||
response = await dispatcher.dispatch(request)
|
response = await dispatcher.dispatch(request)
|
||||||
assert response.status == HTTPStatus.CREATED
|
assert response.status == HTTPStatus.CREATED
|
||||||
|
|
||||||
|
async def test_dispatch_raised_http_exception_passthrough(
|
||||||
|
self,
|
||||||
|
module_context: ModuleContext,
|
||||||
|
) -> None:
|
||||||
|
"""Raised aiohttp HTTP exceptions are passed through."""
|
||||||
|
exc = web.HTTPFound("/dest")
|
||||||
|
|
||||||
|
async def handler(ctx: RouteContext) -> web.Response:
|
||||||
|
raise exc
|
||||||
|
|
||||||
|
dispatcher = _make_dispatcher(module_context)
|
||||||
|
dispatcher.register("/page", handler, module_name="mod")
|
||||||
|
|
||||||
|
request = make_mocked_request("GET", "/owlbot/mod/page")
|
||||||
|
request.match_info["module_name"] = "mod"
|
||||||
|
request.match_info["path"] = "page"
|
||||||
|
|
||||||
|
response = await dispatcher.dispatch(request)
|
||||||
|
assert response is exc
|
||||||
|
|
||||||
async def test_dispatch_none_response(self, module_context: ModuleContext) -> None:
|
async def test_dispatch_none_response(self, module_context: ModuleContext) -> None:
|
||||||
"""Handler returning None produces 204 No Content."""
|
"""Handler returning None produces 204 No Content."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user