Documented streaming route support.

2026-03-26 16:09:28 -04:00
parent 2da7f1fc0f
commit 458065420a
+26 -2
@@ -20,6 +20,7 @@ async def stats(ctx: RouteContext) -> dict:
|-----------|------|---------|-------------|
| `path` | `str` | *(required)* | URL path relative to the module's namespace. |
| `methods` | `list[str] \| None` | `["GET"]` | HTTP methods to accept. |
| `streaming` | `bool` | `False` | If `True`, the handler runs without a timeout. Use for long-lived connections like SSE streams. The handler is responsible for its own keepalives, idle detection, and cleanup. |
A route that accepts multiple methods:
@@ -49,6 +50,28 @@ async def create_data(ctx: RouteContext) -> dict:
Each handler's method set must not overlap with any other handler on the same path. Registering a handler whose methods conflict with an existing one raises `ValueError`.
### Streaming Routes
Routes that hold a connection open for a long time (e.g. Server-Sent Events) should set `streaming=True` to bypass the handler timeout (default 30 seconds):
```python
import asyncio
from aiohttp import web
from owlbot.api import RouteContext, on_route
@on_route("/events", streaming=True)
async def sse_handler(ctx: RouteContext) -> web.StreamResponse:
resp = web.StreamResponse()
resp.content_type = "text/event-stream"
await resp.prepare(ctx.request)
while True:
await resp.write(b"data: ping\n\n")
await asyncio.sleep(30)
```
Streaming handlers manage their own lifecycle. Owlbot does not impose any timeout or keepalive, so a handler that never writes will run indefinitely. Write periodically to detect closed connections, as the next write after a client disconnects will raise `ConnectionResetError`.
## Namespacing
Routes are automatically prefixed under `/owlbot/<module_name>/`. So if the module `stats` registers `@on_route("/dashboard")`, the actual URL is:
@@ -155,7 +178,7 @@ async def incoming_webhook(ctx: RouteContext) -> None:
## Error Handling
If a route handler raises an exception, the error is logged and the client receives an empty `500` response. The same applies if the handler exceeds the configured `handler_timeout` (default 30 seconds); it is cancelled and the client receives a `500`.
If a route handler raises an exception, the error is logged and the client receives an empty `500` response. The same applies if the handler exceeds the configured `handler_timeout` (default 30 seconds); it is cancelled and the client receives a `500`. Streaming handlers (`streaming=True`) are exempt from the timeout.
## URL Building
@@ -276,7 +299,7 @@ async def setup(ctx: ModuleContext) -> None:
| Method | Description |
|--------|-------------|
| `ctx.routes.register(path, handler, methods=...)` | Register a route. Accepts the same parameters as `@on_route`. Returns `RouteInfo`. |
| `ctx.routes.register(path, handler, methods=..., streaming=...)` | Register a route. Accepts the same parameters as `@on_route`. Returns `RouteInfo`. |
| `ctx.routes.unregister(path)` | Remove all handlers at the path. Returns `True` if found. |
| `ctx.routes.unregister(path, method=...)` | Remove only the handler covering that method. Returns `True` if found. |
| `ctx.routes.get(path)` | Get all handlers at the path. Returns `list[RouteInfo]`. |
@@ -295,3 +318,4 @@ async def setup(ctx: ModuleContext) -> None:
| `methods` | `frozenset[str]` | HTTP methods. |
| `handler` | `RouteHandler` | The handler function. |
| `module_name` | `str` | Module that owns this route. |
| `streaming` | `bool` | Whether the handler bypasses the timeout. |