Skip to content

HTTP Requests

UI events say what the host observed (a key, a click, a tick). Request hooks say what arrived over HTTP.

They live in xnano.requests: method decorators (@on_get_request, @on_post_request, …), a generic request(...) helper, and the Request / Response types. Handlers are methods on a BaseGrid. Routes are collected when that grid is served — under Web, or via RequestServer.

A plain Terminal session does not open HTTP ports. Request hooks on a grid do nothing until a server that understands them is running.

Experimental

HTTP request hooks (xnano.requests) and the request-oriented server path are experimental. The API may still change.

Serving HTTP

Host / entry Opens a port? Serves request hooks?
Terminal / Terminal.run(...) No No — TUI only
Web(state=...).run(..., host=..., port=...) Yes Yes — cell UI + declared routes
RequestServer / start_request_server / serve_requests Yes Yes — HTTP only, no browser shell

Web binds host / port on run(). Terminal never takes those arguments. Examples below assume a normal process that can listen on a socket — they are not Pyodide demos.

Declaring routes on a grid

Decorate a grid method with the HTTP verb and path. Paths are normalized to a leading slash ("status""/status"; empty → "/").

All method decorators share the same call shapes:

@on_get_request                 # path defaults to "/"
@on_get_request("/status")
@on_get_request(path="/status")

Return a Response, a non-None value (wrapped as Response(body=str(...))), or None (empty successful response when a route matched). Mutate grid fields or application state the same way UI hooks do; the next paint reflects it under Web.

GET requests

GET handler
from xnano import BaseGrid, Field, Web
from xnano.requests import Response, on_get_request

class Status(BaseGrid):
    body: str = Field(default="ok")

    @on_get_request("/status")
    def status(self) -> Response:
        return Response.json({"ok": True, "body": self.body})

Web().run(Status(), port=8000)
Call the route
curl http://127.0.0.1:8000/status

HEAD requests

HEAD uses the same path matching as GET. Handlers still run; servers that speak the full method set omit the response body on the wire for HEAD (see RequestServer).

HEAD handler
from xnano.requests import Response, on_head_request

@on_head_request("/status")
def status_head(self) -> Response:
    return Response(status=200, headers={"x-ready": "1"})

POST requests

POST with body and state
import dataclasses

from xnano import BaseGrid, Field, Web
from xnano.requests import Response, on_post_request

@dataclasses.dataclass
class Counters:
    hits: int = 0

class Counter(BaseGrid):
    label: str = Field(default="hits: 0")

    @on_post_request("/hit")
    def hit(self, ctx) -> Response:
        ctx.state.hits += 1
        self.label = f"hits: {ctx.state.hits}"
        return Response.json({"hits": ctx.state.hits})

Web(state=Counters()).run(Counter(), port=8000)
POST the route
curl -X POST http://127.0.0.1:8000/hit

Read JSON from the parsed request with ctx.request.json() when the client sends a body (see Request / Response).

PUT requests

PUT replace
from xnano import BaseGrid, Field, Web
from xnano.requests import Response, on_put_request

class Document(BaseGrid):
    content: str = Field(default="")

    @on_put_request("/document")
    def put_document(self, ctx) -> Response:
        payload = ctx.request.json() if ctx.request else None
        self.content = str((payload or {}).get("content", ""))
        return Response.json({"content": self.content})

Web().run(Document(), port=8000)
curl -X PUT http://127.0.0.1:8000/document \
  -H 'content-type: application/json' \
  -d '{"content": "hello"}'

PATCH requests

PATCH partial update
from xnano import BaseGrid, Field, Web
from xnano.requests import Response, on_patch_request

class Profile(BaseGrid):
    name: str = Field(default="anonymous")

    @on_patch_request("/profile")
    def patch_profile(self, ctx) -> Response:
        payload = ctx.request.json() if ctx.request else {}
        if "name" in (payload or {}):
            self.name = str(payload["name"])
        return Response.json({"name": self.name})

Web().run(Profile(), port=8000)
curl -X PATCH http://127.0.0.1:8000/profile \
  -H 'content-type: application/json' \
  -d '{"name": "midoriya"}'

DELETE requests

DELETE
from xnano import BaseGrid, Field, Web
from xnano.requests import Response, on_delete_request

class Session(BaseGrid):
    active: bool = Field(default=True, state=True)

    @on_delete_request("/session")
    def end_session(self) -> Response:
        self.active = False
        return Response(status=204)

Web().run(Session(), port=8000)
curl -X DELETE http://127.0.0.1:8000/session

Other HTTP methods

Less common verbs use the same decorator pattern and path rules:

Decorator Method
@on_connect_request CONNECT
@on_options_request OPTIONS
@on_trace_request TRACE
@on_query_request QUERY
OPTIONS
from xnano.requests import Response, on_options_request

@on_options_request("/items")
def items_options(self) -> Response:
    return Response(
        status=204,
        headers={"allow": "GET, POST, OPTIONS"},
    )

Generic method registration

When the method is dynamic or you prefer one entry point, use request(method, path):

Generic method registration
from xnano import BaseGrid
from xnano.requests import Response, request

class App(BaseGrid):
    @request("PATCH", "/thing")
    def update(self, ctx) -> Response:
        payload = ctx.request.json() if ctx.request else {}
        return Response.json({"name": (payload or {}).get("name")})

method is uppercased. Unsupported method names raise ValueError. Supported values match the dedicated decorators: GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH, QUERY.

Request and response objects

Incoming request

Immutable parsed request available as ctx.request when the server builds one (both Web / native path and RequestServer do).

Attribute Meaning
method Uppercase HTTP method
path Normalized path with leading /
query Multi-value mapping: strtuple[str, ...]
headers Header map (keys lowercased on from_parts)
body Raw bytes

Helpers:

  • request.text(encoding="utf-8") — decode body
  • request.json() — parse JSON (None if body is empty)
  • Request.from_parts(method, path, *, query_string=..., headers=..., body=..., max_body=...) — build from raw parts; raises ValueError if body exceeds max_body (default 1 MiB)

Outgoing response

What a handler returns to the client.

Attribute Default Meaning
body b"" bytes or str
status 200 HTTP status code
headers {} Response headers
Response.json
from xnano.requests import Response

Response.json({"ready": True}, status=201)
# content-type: application/json; charset=utf-8

as_bytes() encodes a string body as UTF-8. If the handler returns a non-bool value that is not a Response, dispatch wraps it as Response(body=str(result)). With a runtime attached, unmatched routes become 404 with body Not Found.

Handlers and context

Handlers that accept a ctx parameter receive the same Context shape as UI hooks.

Field In request hooks
ctx.state Application state from Web(state=...) or the runtime
ctx.request Parsed Request, or None if not supplied to dispatch
ctx.terminal / ctx.runtime Host / runtime facade used for the dispatch
ctx.state and ctx.request
@on_post_request("/submit")
def submit(self, ctx) -> Response:
    mode = ctx.request.query.get("mode", ())
    data = ctx.request.json()
    ctx.state.last = data
    return Response.json({"mode": mode, "accepted": True}, status=202)

Handlers that omit ctx still work when they only need self.

Named request actions

Action.request(method, path) names an HTTP trigger the same way Action.keyboard names a key. Pair it with @on_action when you want one constant for both synthetic perform and real request matching.

Action.request
from xnano import Action, BaseGrid, on_action
from xnano.requests import Response, on_get_request

GET_STATUS = Action.request("GET", "/status")

class Api(BaseGrid):
    @on_get_request("/status")
    def status(self) -> Response:
        return Response.json({"ok": True})

    @on_action(GET_STATUS)
    def on_status_action(self) -> None:
        ...

The actions performer also exposes ctx.actions.request(method, path) as a shortcut for perform(Action.request(...)). See Actions.

Web host and request servers

Entry Role
Web(state=...).run(source, host=..., port=...) Browser cell UI + request hooks for the served grid
xnano.server.native.serve_native Lower-level native cell server used by Web
xnano.server.requests.RequestServer ThreadingHTTPServer for one grid’s request hooks
start_request_server(grid, host=..., port=..., runtime=...) Bind on a daemon thread; returns the server
serve_requests Alias of start_request_server

Request-only example (no browser shell):

RequestServer
from xnano import BaseGrid
from xnano.requests import Response, on_get_request
from xnano.server.requests import start_request_server

class Health(BaseGrid):
    @on_get_request("/health")
    def health(self) -> Response:
        return Response.json({"ok": True})

server = start_request_server(Health, host="127.0.0.1", port=8000)
# server.shutdown(); server.server_close()
curl http://127.0.0.1:8000/health

port=0 selects a free port; read it from server.server_address[1].

Body size is capped (about 1 MiB) on the request server path; oversized bodies get 413. See the server API.

Paths and routing

Matching is exact on method and normalized path:

  • Paths are stripped and given a leading /. Empty paths become "/".
  • Query strings are not part of the path key. They appear on ctx.request.query after the route matches.
  • There is no path-parameter or wildcard syntax in the current matcher — "/items" does not match "/items/1".
  • Registration order is preserved. A name defined on a more-derived class shadows the same method name from a base class when hooks are collected.
  • Private methods (_name) are skipped unless they carry a request-hook marker.
Path normalization
@on_get_request("status")   # registers as "/status"
@on_get_request("")         # registers as "/"
@on_get_request("/")        # registers as "/"

Next

API

xnano.requests · Request · Response · Web · RequestServer · Action.request