A Request is what a handler annotated to take one receives.
Its reads copy out of the received bytes: verb, path, query_string, path_parameter(name), query(name) and header(name), with peer naming where it came from. path, query_string, path parameters and query values stay percent-encoded as sent, header names match case-insensitively, a lookup that finds nothing answers None, and the body arrives whole, decoded from chunked framing if it was sent that way. fields() lists every header field in arrival order as (name, value) bytes, names lowercased and values trimmed, as a proxy forwards a head. peer is a tuple of the interface’s index in the server’s arguments, the client’s 32-byte static key, 32 zero bytes on a plaintext interface, and its address: an (ipaddress, port) pair, "" for an AF_UNIX peer, or None.
import ucallserver = ucall.Server(ucall.Interface("127.0.0.1:0"))@server.post("/orders/{order}")def order(request: ucall.Request): token = request.header("authorization") if token is None: request.reply_error(401, "sign in first\n") return receipt = f"{request.path_parameter('order')} by {token}".encode() request.reply(receipt, status=201, headers=("content-type: text/plain", "cache-control: no-store"))
request.body is a read-only memoryview over the received bytes, and the request itself supports the buffer protocol, so bytes(request) copies the body and numpy.frombuffer(request, numpy.int8) views it without a copy.
Both views stay valid until the reply, which frees those bytes, so a reply is refused with BufferError while any of them is alive.
The one view a reply accepts is request.body itself passed as the reply body, which it releases first, so request.reply(request.body) echoes without a copy.
reply(body, status, headers) sends any bytes-like body, copied before it returns, with “name: value” header lines, and Content-Length, Date and Server are generated; reply_error(status, message) sends a plain-text error.
Either consumes the request, so a second reply raises ucall.Error with Status.ALREADY_EXISTS, and later reads raise ucall.Error or, for the body, BufferError.
A request may be kept past its handler and answered later, from any thread, once, while responses still leave each connection in request order.
A request released without a reply answers 500.
A reply to a closed connection still consumes the request silently; any other failed reply raises ucall.Error and keeps it.
request.path_segments is a tuple of the path’s components as bytes, and request.parts a tuple of ucall.RequestPart(name, content_type, data) for a multipart/form-data body.
Each part’s data views the received bytes like body does, and keeps the request alive and unanswered until every such view is gone.
class BodyChunks(Iterator[memoryview], AsyncIterator[memoryview])
async for returns a chunk already decoded at once and waits for one still arriving in the
running loop's default executor; for waits on the calling thread.
Examples
>>> async for chunk in detached.body_chunks(): # or: for chunk in detached.body_chunks()... digest.update(chunk)
ClassRequestOne received HTTP request, retained until a reply consumes it.14
A handler taking one parameter annotated Request receives it on the polling thread, and may
keep it to answer later from any thread, once, or detach it to a worker for the replies that
may defer. body, parts and the buffer protocol view the received bytes without a copy
until the reply, which is refused while such a view is alive, except the body passed as the
reply body itself. A request released without a reply answers 500.
The DetachedRequest owns the request from then on, and this object answers nothing. A
Deferred waits when released, and a wait on the polling thread would wait for the loop
running it, so pass the detached request to a worker rather than replying here.
Raises
BufferError: A view over body or a part is still alive.
peerThe index of the interface it arrived on, the peer's 32-byte Noise key, zeros on plaintext, and its address: (ipaddress, port), "" for AF_UNIX, or None.
data views the received bytes without a copy and keeps the request alive, so the request
refuses its reply until every such view is gone, as it does for views over body.
Examples
>>> @server.post("/upload")... def upload(request: ucall.Request):... return {part.name: len(part.data) for part in request.parts}
It owns the reply's outcome and keeps the File parts it borrows alive until the outcome is
final. await it in a coroutine, or call wait(); released unfinished, it waits, so keep it
off the polling thread, as Request.detach does.
It reads and answers as Request does, and alone offers reply_parts, reply_stream and
body_chunks, whose Deferred replies and waiting pulls belong on a worker thread. Released
without a reply, it answers 500.
Bytes-like parts are copied before the call returns, and the reply is then final. A File or
Path part is read by the event loop afterwards, sized first so Content-Length is measured,
and its reply defers: one failing before the head replaces the reply with 404 or 416, and one
failing after it resets the connection.
The body is chunked for HTTP/1.1 peers, unless headers declare Content-Length, which the
parts must then sum to. A peer that left already still opens the stream, whose parts drop.
Parameters
statusint
The HTTP status. Defaults to 200.
headersSequence[str | bytes]
"name: value" lines.
Returns
The open reply, which owns the request from then on.
Raises
BufferError: A view over the request's bytes is still alive.
Each chunk is a read-only memoryview valid until the next pull, which releases it, and a
pull waits for bytes without the interpreter lock. On a buffered endpoint the first chunk
is the whole body.
Returns
The iterator, ending once the body did.
Raises
BufferError: From a pull, while a view derived from the last chunk is still alive.
peerThe index of the interface it arrived on, the peer's 32-byte Noise key, zeros on plaintext, and its address: (ipaddress, port), "" for AF_UNIX, or None.