App¶
App
¶
App(
title: str = "Oxbrook",
version: str = "0.1.0",
description: str = "",
openapi_url: str | None = "/openapi.json",
docs_url: str | None = "/docs",
mcp_url: str | None = "/mcp",
debug: bool = False,
access_log: bool = False,
redis_url: str | None = None,
lifespan: Any = None,
worker_lifespan: Any = None,
cors: CORS | None = None,
websocket_origins: Any = None,
)
openapi_url and docs_url can each be set to None to disable them.
debug returns handler exception text in the 500 response. Leave it off
outside development: exception messages routinely carry connection
strings, file paths and user data.
access_log adds one log line per request. Opt-in: it costs something
per request, and many deployments already log at the proxy.
redis_url enables durable topics. Nothing connects until the first
durable topic is used.
mcp_url is where agents reach the service over the Model Context
Protocol. It exposes only routes marked tool=True, plus topics as
readable resources. Set it to None to turn the endpoint off entirely.
lifespan runs once around the server's life and worker_lifespan runs
on every worker loop; what they yield is request.state. Two, because
a connection pool belongs to the loop that made it and a server has
several loops. See the lifespan guide.
cors lets pages on other origins call the app from a browser. Applied
in Rust, to every response including the ones no handler produced.
websocket_origins lists the other origins whose pages may open a
WebSocket. Browsers do not apply CORS to sockets and send cookies with
the handshake, so without a check any website could open an
authenticated socket as the user. A socket is accepted with no
Origin (not a browser), from this server's own origin, or from a
listed one; anything else is refused with 403 before an authorizer or
handler runs. Left unset, the list is the CORS origins, excluding *.
["*"] turns the check off.
Source code in python/oxbrook/_app.py
worker_lifespan
instance-attribute
¶
route
¶
Register a route.
tool=True also exposes it to agents over MCP. Opt-in on purpose:
every route being agent-callable by default would mean an
administrative delete endpoint is agent-callable by default.
Source code in python/oxbrook/_app.py
get
¶
post
¶
put
¶
patch
¶
delete
¶
include
¶
include(router: Router, prefix: str = '') -> None
Mount a router's routes, under prefix if given.
app.include(users.router, prefix="/api/v1")
Every route is validated against its full path here, and checked against the routes already registered, so a conflict between two modules fails at this call rather than at startup. Include after the router is fully declared: it cannot change afterwards.
Source code in python/oxbrook/_app.py
middleware
¶
Register middleware, which runs around every HTTP handler.
@app.middleware
async def require_key(request, call_next):
if request.header("x-api-key") != SECRET:
return Reply({"error": "unauthorized"}, status=401)
return await call_next(request)
Runs outermost-first in registration order. WebSocket routes are not wrapped: their handshake completes before the handler runs, so there is nothing useful to intercept yet.
Source code in python/oxbrook/_app.py
exception_handler
¶
Register how an exception becomes a response.
@app.exception_handler(LookupError)
async def missing(request, exc):
return Reply({"error": "not found"}, status=404)
Applies to the class and its subclasses; the most specific registered class wins. Raised by a handler, a dependency, body validation, an authorizer or middleware, the exception is mapped before middleware sees the result, so the access log records the real status.
Registering HTTPError or RequestValidationError replaces the
built-in response for it. See oxbrook.HTTPError.
Source code in python/oxbrook/_app.py
websocket
¶
Register a WebSocket endpoint.
authorize runs before the handshake and can refuse the upgrade,
which the handler cannot: by the time it runs, the 101 has been sent
and the client believes it is connected. Return None or True to
accept, or a Response/Reply to refuse.
async def members_only(request):
if not valid(request.header("authorization")):
return Response(b"nope", status=401, content_type="text/plain")
@app.websocket("/ws", authorize=members_only)
async def feed(request, ws): ...
The handler takes the request and the socket. Oxbrook performs the handshake, so the socket is already open when the handler runs, and the connection closes when it returns.
@app.websocket("/ws")
async def echo(request, ws):
async for message in ws:
await ws.send(message)
Source code in python/oxbrook/_app.py
backend
¶
The shared Redis backend, connected lazily on first use.
Source code in python/oxbrook/_app.py
topic
¶
topic(
name: str,
maxsize: int | None = None,
policy: str | None = None,
durable: bool = False,
) -> Topic
Get or create a named topic.
Shared across every worker loop in the process, so a message emitted by
one handler reaches subscribers running on all of them. durable=True
additionally shares it across processes and records it in Redis, which
needs App(redis_url=...).
maxsize, policy and durable apply only when the topic is first
created.
Source code in python/oxbrook/_app.py
capabilities
¶
The capabilities this service exposes to agents.
Built from the routes marked tool=True, without starting a server, so
it can be inspected or checked into a test.
Source code in python/oxbrook/_app.py
openapi
¶
The OpenAPI 3.1 document for the routes registered so far.
Built from the same metadata the router uses, so it cannot describe an endpoint the server would not accept. Callable without running the server, which makes it usable for client generation in CI.
Source code in python/oxbrook/_app.py
run
¶
run(
host: str = "127.0.0.1",
port: int = 8000,
workers: int | None = None,
max_concurrency: int = DEFAULT_MAX_CONCURRENCY,
max_body: int = DEFAULT_MAX_BODY,
request_timeout: float = DEFAULT_REQUEST_TIMEOUT,
shutdown_grace: float = DEFAULT_SHUTDOWN_GRACE,
max_connections: int = DEFAULT_MAX_CONNECTIONS,
max_message: int = DEFAULT_MAX_MESSAGE,
) -> None
Serve until interrupted.
max_concurrency bounds the requests a single worker loop will accept
at once, counting both those queued and those already running. When
every worker is at its limit the server answers 503 rather than growing
without bound. Lower it for slow handlers, where a deep backlog only
adds latency before an inevitable client timeout; raise it to absorb
larger bursts of fast requests.
max_body caps a request body; anything larger is answered 413 without
being buffered. max_message does the same for a single WebSocket
message, where the connection is closed rather than answered.
request_timeout is how long to wait for a handler's first response
before answering 504. It does not cut short a stream that has already
started, so SSE and WebSocket are unaffected. Zero disables it.
shutdown_grace is how long Ctrl-C waits for in-flight requests to
finish before stopping anyway.
max_connections caps sockets held open. At the limit the server stops
accepting rather than refusing, so the wait lands in the OS backlog.
Source code in python/oxbrook/_app.py
build_server
¶
build_server(
host: str = "127.0.0.1",
port: int = 8000,
workers: int | None = None,
max_concurrency: int = DEFAULT_MAX_CONCURRENCY,
max_body: int = DEFAULT_MAX_BODY,
request_timeout: float = DEFAULT_REQUEST_TIMEOUT,
shutdown_grace: float = DEFAULT_SHUTDOWN_GRACE,
max_connections: int = DEFAULT_MAX_CONNECTIONS,
max_message: int = DEFAULT_MAX_MESSAGE,
announce: bool = False,
)
Prepare a server without starting it.
run uses this; so does the test client, which needs to start the
server on one thread and stop it from another. The lifespans run inside
serve, not here.
Source code in python/oxbrook/_app.py
Router
¶
A group of routes with a shared prefix and middleware.
Source code in python/oxbrook/_routers.py
route
¶
Register a route. See App.route.
Source code in python/oxbrook/_routers.py
get
¶
post
¶
put
¶
patch
¶
delete
¶
websocket
¶
Register a WebSocket endpoint. See App.websocket.
middleware
¶
Middleware for this router's routes only, inside the app's own.
Covers routes on routers included into this one as well. Like app middleware it wraps a socket's authorizer, not the socket handler.
Source code in python/oxbrook/_routers.py
HTTPError
¶
Bases: Exception
Raise to answer with an error status.
raise HTTPError(404, "no such user")
raise HTTPError(401, headers={"www-authenticate": "Bearer"})
detail is sent to the client, so it is for messages written for the
client. It defaults to the status's standard phrase. This is the one
exception whose text is returned: an unhandled exception's never is.
Source code in python/oxbrook/_errors.py
RequestValidationError
¶
Bases: Exception
A request body failed validation. Carries a ready-to-send JSON body.