# Requests

> UCall for Python

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.

```python
import ucall

server = 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.

`import ucall`

`from ucall import Request, RequestPart, Deferred, …`

## BodyChunks

The iterator [`DetachedRequest.body_chunks`](#body_chunks) returns, yielding memoryviews of the body.

```python
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)
```

## Request

One received HTTP request, retained until a reply consumes it.

```python
class Request(Buffer)
```

A handler taking one parameter annotated [`Request`](#Request) receives it on the polling thread, and may
keep it to answer later from any thread, once, or [`detach`](#detach) it to a worker for the replies that
may defer. [`body`](https://www.unum.cloud/docs/ucall/python/calling.md#body), [`parts`](#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`](https://www.unum.cloud/docs/ucall/python/calling.md#body) passed as the
reply body itself. A request released without a reply answers 500.

Examples: ```
>>> @server.put("/echo/{name}")
... def echo(request: ucall.Request):
...     interface_index, static_key = request.peer
...     greeting = f"{request.verb.name} {request.path_parameter('name')}"
...     request.reply(request.body, headers=(f"x-greeting: {greeting}",))
```

## *

Looks up a header of the request by its case-insensitive name.

```python
def header(name: str | bytes, /) -> str | None
```

- `name`: The header name, such as "Content-Type".

Examples: ```
>>> @server.get("/agent")
... def agent(request: ucall.Request):
...     return request.header("user-agent") or "unknown"
```

## *

Every header field line in arrival order, repeated names apart, as a proxy forwards a head.

```python
def fields() -> list[tuple[bytes, bytes]]
```

Examples: ```
>>> [name for name, _ in request.fields()]
[b'host', b'accept']
```

## *

Looks up a query parameter of the request by its case-sensitive name.

```python
def query(name: str | bytes, /) -> str | None
```

- `name`: The parameter name.

Examples: ```
>>> @server.get("/search")
... def search(request: ucall.Request):
...     return urllib.parse.unquote_plus(request.query("q") or "")
```

## path_*_parameter

Looks up a placeholder the endpoint's URI template captured.

```python
def path__parameter(name: str | bytes, /) -> str | None
```

- `name`: The placeholder name without braces, "path" for `{*path}`.

Examples: ```
>>> @server.get("/files/{*path}")
... def files(request: ucall.Request):
...     return request.path_parameter("path")
```

## *

Answers the request, once, from any thread; a peer that disconnected consumes it silently.

```python
def reply(
    body: Buffer = b'',
    status: int = 200,
    headers: Sequence[str | bytes] = (),
) -> None
```

- `body`: Any bytes-like object, copied before the call returns; the
request's own [`body`](https://www.unum.cloud/docs/ucall/python/calling.md#body) passes without a copy and is released by the reply.
- `status`: The HTTP status. Defaults to 200.
- `headers`: "name: value" lines; the server adds
Content-Length, Date and Server itself.

Raises: - `BufferError`: A view over [`body`](https://www.unum.cloud/docs/ucall/python/calling.md#body), other than the one passed, is still alive.

- [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The request was already answered, with [`Status.ALREADY_EXISTS`](https://www.unum.cloud/docs/ucall/python/errors.md#ALREADY_EXISTS).

Examples: ```
>>> @server.post("/echo")
... def echo(request: ucall.Request):
...     request.reply(request.body, headers=("content-type: application/octet-stream",))
```

## reply_*_error

Answers the request with an error status and a plain-text message, once.

```python
def reply__error(status: int, message: str | bytes) -> None
```

- `status`: The HTTP status, 400 or above.
- `message`: The body, sent as text/plain.

Raises: - `BufferError`: A view over [`body`](https://www.unum.cloud/docs/ucall/python/calling.md#body) is still alive.

- [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The request was already answered, with [`Status.ALREADY_EXISTS`](https://www.unum.cloud/docs/ucall/python/errors.md#ALREADY_EXISTS).

Examples: ```
>>> @server.get("/teapot")
... def teapot(request: ucall.Request):
...     request.reply_error(418, "short and stout\n")
```

## detach

Hands the request to another thread, the one way to the verbs whose replies may defer.

```python
def detach() -> DetachedRequest
```

The [`DetachedRequest`](#DetachedRequest) owns the request from then on, and this object answers nothing. A
[`Deferred`](#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`](https://www.unum.cloud/docs/ucall/python/calling.md#body) or a part is still alive.

- [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The request was already answered or detached, with [`Status.ALREADY_EXISTS`](https://www.unum.cloud/docs/ucall/python/errors.md#ALREADY_EXISTS).

Examples: ```
>>> @server.get("/videos/{name}")
... def video(request: ucall.Request):
...     workers.submit(send_video, request.detach(), request.path_parameter("name"))
```

## verb

The [`Verb`](https://www.unum.cloud/docs/ucall/python/serving.md#Verb) it arrived with, never [`Verb.ANY`](https://www.unum.cloud/docs/ucall/python/serving.md#ANY).

```python
verb: Verb
```

## path

The path as sent, still percent-encoded, without the query.

```python
path: str
```

## body

A read-only memoryview of the body, valid until the reply.

```python
body: memoryview
```

## peer

The 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.

```python
peer: tuple[int, bytes, tuple[IPv4Address | IPv6Address, int] | str | None]
```

## parts

The [`RequestPart`](#RequestPart)s of a multipart/form-data body, else empty.

```python
parts: tuple[RequestPart, ...]
```

## path_segments

The path's components between slashes, as sent.

```python
path_segments: tuple[bytes, ...]
```

## query_string

The query after the first `?`, still percent-encoded, or "" without one.

```python
query_string: str
```

## RequestPart

One part of a `multipart/form-data` request body, from [`Request.parts`](#parts).

```python
class RequestPart
```

[`data`](#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`](https://www.unum.cloud/docs/ucall/python/calling.md#body).

Examples: ```
>>> @server.post("/upload")
... def upload(request: ucall.Request):
...     return {part.name: len(part.data) for part in request.parts}
```

## name

The name from Content-Disposition.

```python
name: str
```

## content_type

The part's Content-Type, empty if absent.

```python
content_type: str
```

## data

A read-only memoryview of the payload, which refuses the request's reply while alive.

```python
data: memoryview
```

## Deferred

A reply still sending file or path parts, from [`reply_parts`](#reply_parts) or [`ReplyStream.end`](https://www.unum.cloud/docs/ucall/python/files-and-streams.md#end).

```python
class Deferred(Awaitable[None])
```

It owns the reply's outcome and keeps the [`File`](https://www.unum.cloud/docs/ucall/python/files-and-streams.md#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`](#detach) does.

Examples: ```
>>> deferred = detached.reply_parts([b"--frame\r\n", ucall.Path(0, "intro.mp4")])
>>> deferred.wait()  # or: await deferred
```

## status

Reads the reply's outcome without waiting.

```python
def status() -> Status
```

Examples: ```
>>> deferred.status() in [ucall.Status.DEFERRED, ucall.Status.SUCCESS]
True
```

## wait

Blocks until every part is sent, without the interpreter lock.

```python
def wait() -> None
```

Raises: - [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): The reply failed, such as [`Status.NOT_FOUND`](https://www.unum.cloud/docs/ucall/python/errors.md#NOT_FOUND) for a missing file, which answered
404, or [`Status.CONNECTION_CLOSED`](https://www.unum.cloud/docs/ucall/python/errors.md#CONNECTION_CLOSED) for a peer that left.

Examples: ```
>>> deferred.wait()
```

## DetachedRequest

A request taken off its polling thread by [`Request.detach`](#detach), owning the replies that may defer.

```python
class DetachedRequest(Buffer)
```

It reads and answers as [`Request`](#Request) does, and alone offers [`reply_parts`](#reply_parts), [`reply_stream`](#reply_stream) and
[`body_chunks`](#body_chunks), whose [`Deferred`](#Deferred) replies and waiting pulls belong on a worker thread. Released
without a reply, it answers 500.

Examples: ```
>>> def send_video(detached: ucall.DetachedRequest, name: str):
...     deferred = detached.reply_parts([b"--frame\r\n", ucall.Path(0, name)])
...     deferred.wait()  # or: await deferred
```

## reply_parts

Answers the request, once, with [`parts`](#parts) concatenated into the body.

```python
def reply_parts(
    parts: Sequence[Buffer | File | Path],
    status: int = 200,
    headers: Sequence[str | bytes] = (),
) -> Deferred | None
```

Bytes-like parts are copied before the call returns, and the reply is then final. A [`File`](https://www.unum.cloud/docs/ucall/python/files-and-streams.md#File) or
[`Path`](https://www.unum.cloud/docs/ucall/python/files-and-streams.md#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.

- `parts`: The body, in order.
- `status`: The HTTP status. Defaults to 200.
- `headers`: "name: value" lines; the server adds
Content-Length, Date and Server itself.

Raises: - `BufferError`: A view over the request's bytes is still alive.

- [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): A refused part, such as a descriptor without O_DIRECT, or a request already
answered, with [`Status.ALREADY_EXISTS`](https://www.unum.cloud/docs/ucall/python/errors.md#ALREADY_EXISTS).

Examples: ```
>>> deferred = detached.reply_parts([b"--frame\r\n", ucall.Path(0, "intro.mp4")])
>>> deferred.wait()  # or: await deferred
```

## reply_stream

Answers the request with a body sent in parts, emitting the status line and headers now.

```python
def reply_stream(
    status: int = 200,
    headers: Sequence[str | bytes] = (),
) -> ReplyStream
```

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.

- `status`: The HTTP status. Defaults to 200.
- `headers`: "name: value" lines.

Raises: - `BufferError`: A view over the request's bytes is still alive.

- [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): A malformed header, or a request already answered, with [`Status.ALREADY_EXISTS`](https://www.unum.cloud/docs/ucall/python/errors.md#ALREADY_EXISTS).

Examples: ```
>>> stream = detached.reply_stream(headers=("content-type: text/event-stream",))
>>> stream.append([b"data: 1\n\n"])
>>> stream.end()
```

## body_chunks

Iterates the request body as it arrives, each chunk the bytes decoded since the last pull.

```python
def body_chunks() -> BodyChunks
```

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.

Raises: - `BufferError`: From a pull, while a view derived from the last chunk is still alive.

- [`Error`](https://www.unum.cloud/docs/ucall/python/errors.md#Error): From a pull, [`Status.CONNECTION_CLOSED`](https://www.unum.cloud/docs/ucall/python/errors.md#CONNECTION_CLOSED) or [`Status.MALFORMED`](https://www.unum.cloud/docs/ucall/python/errors.md#MALFORMED) for a body cut short
or badly framed, or [`Status.WOULD_BLOCK`](https://www.unum.cloud/docs/ucall/python/errors.md#WOULD_BLOCK) on the polling thread with nothing decoded.

Examples: ```
>>> async for chunk in detached.body_chunks():  # or: for chunk in detached.body_chunks()
...     digest.update(chunk)
```

## verb

The [`Verb`](https://www.unum.cloud/docs/ucall/python/serving.md#Verb) it arrived with, never [`Verb.ANY`](https://www.unum.cloud/docs/ucall/python/serving.md#ANY).

```python
verb: Verb
```

## path

The path as sent, still percent-encoded, without the query.

```python
path: str
```

## body

A read-only memoryview of the body, valid until the reply.

```python
body: memoryview
```

## peer

The 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.

```python
peer: tuple[int, bytes, tuple[IPv4Address | IPv6Address, int] | str | None]
```

## parts

The [`RequestPart`](#RequestPart)s of a multipart/form-data body, else empty.

```python
parts: tuple[RequestPart, ...]
```

## path_segments

The path's components between slashes, as sent.

```python
path_segments: tuple[bytes, ...]
```

## query_string

The query after the first `?`, still percent-encoded, or "" without one.

```python
query_string: str
```

