Documented streaming route support.
+26
-2
@@ -20,6 +20,7 @@ async def stats(ctx: RouteContext) -> dict:
|
|||||||
|-----------|------|---------|-------------|
|
|-----------|------|---------|-------------|
|
||||||
| `path` | `str` | *(required)* | URL path relative to the module's namespace. |
|
| `path` | `str` | *(required)* | URL path relative to the module's namespace. |
|
||||||
| `methods` | `list[str] \| None` | `["GET"]` | HTTP methods to accept. |
|
| `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:
|
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`.
|
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
|
## Namespacing
|
||||||
|
|
||||||
Routes are automatically prefixed under `/owlbot/<module_name>/`. So if the module `stats` registers `@on_route("/dashboard")`, the actual URL is:
|
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
|
## 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
|
## URL Building
|
||||||
|
|
||||||
@@ -276,7 +299,7 @@ async def setup(ctx: ModuleContext) -> None:
|
|||||||
|
|
||||||
| Method | Description |
|
| 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)` | 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.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]`. |
|
| `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. |
|
| `methods` | `frozenset[str]` | HTTP methods. |
|
||||||
| `handler` | `RouteHandler` | The handler function. |
|
| `handler` | `RouteHandler` | The handler function. |
|
||||||
| `module_name` | `str` | Module that owns this route. |
|
| `module_name` | `str` | Module that owns this route. |
|
||||||
|
| `streaming` | `bool` | Whether the handler bypasses the timeout. |
|
||||||
|
|||||||
Reference in New Issue
Block a user