Context and responses
Handlers receive a single Context (c), Hono-style. c.req wraps the
standard Fetch Request (raw object at c.req.raw) and adds routing
context; helpers build standard Response objects.
Reading the request
@app.post("/upload")
async def upload(c):
c.req.method # "POST"
c.req.url.pathname # WHATWG URL
c.req.header("content-type") # combined per Fetch
c.req.cookies # dict, RFC 6265bis parsing
data = await c.req.json() # or .text() / .bytes() / .form_data()
Bodies are one-shot per the Fetch Standard (body_used); c.req.raw.clone()
tees the stream when you need to read twice.
Building responses
return c.json({"ok": True}) # application/json
return c.text("hello") # text/plain;charset=utf-8
return c.html("<h1>hi</h1>") # text/html
return c.body(raw_bytes, 200, headers={...}) # anything
return c.redirect("/next", 303)
return Response(None, 204) # raw Response always works
Streaming is just an async iterable of bytes:
async def numbers():
for i in range(5):
yield f"{i}\n".encode()
return c.body(numbers())
Errors: RFC 9457 everywhere
raise HTTPException(422, title="Unprocessable", detail="year must be > 0",
extensions={"field": "year"})
produces application/problem+json. Framework-generated 404/405/500 use the
same format. Override with @app.on_error / @app.not_found.
Cross-cutting state and headers
@app.use
async def auth(c, next_):
c.set("user", await lookup_user(c)) # typed hand-off to handlers
await next_()
c.header("x-served-by", "hayate") # merged into the final response
@app.get("/me")
async def me(c):
return c.json({"user": c.get("user")})
After the response: c.wait_until
@app.post("/orders")
async def order(c):
c.wait_until(send_confirmation_email()) # runs after delivery
return c.json({"ok": True}, 201)
Semantics match the Workers ctx.waitUntil: on ASGI the work is awaited
after the response is sent; on Cloudflare Workers it forwards to the
platform's own waitUntil.
Cookies
c.set_cookie("sid", token, http_only=True, secure=True, same_site="lax")
__Host-/__Secure- prefix invariants and SameSite=None-requires-Secure
are enforced at serialization time. HMAC signing helpers live in
hayate.cookies (sign_value / unsign_value).