Rust · Requests

Requests

use ucall::DetachedRequest;use ucall::Peer;use ucall::PeerAddress;use ucall::ReplyPart;use ucall::Request;use ucall::RequestPart;use ucall::BytesView;use ucall::Deferred;use ucall::HTTPStatus;use ucall::PercentMode;use ucall::Wait;use ucall::*; 1.0.0-rc.1 · 60202332

A handler receives each Request by value. Its reads borrow the received bytes: verb, path, query_string, path_parameter, query, header and body, with peer naming the interface it arrived on, the client’s authenticated Noise key and its PeerAddress: Ip with the port and IPv6 scope, Unix, or Unknown. The path and query values stay percent-encoded as sent, which ucall::decode_percent decodes, header names match case-insensitively, and the body arrives whole, decoded from chunked framing if it was sent that way. fields iterates every header field in arrival order as (name, value), names lowercased and values trimmed, as a proxy forwards a head. path_segments_count and path_segment read the path component by component, and parts_count and part read a multipart/form-data body as RequestParts, each borrowing its name, content type and payload.

use ucall::*;

let mut server = Server::new(&[Interface::new("127.0.0.1:0")])?;
server.post("/orders/{order}", |request: Request| {
    let Some(token) = request.header("authorization") else {
        let _ = request.reply_error(HTTPStatus::UNAUTHORIZED, "sign in first");
        return;
    };
    let receipt = [request.path_parameter("order").unwrap_or_default(), b" by ", token].concat();
    let headers = ["content-type: text/plain".into(), "cache-control: no-store".into()];
    let _ = request.reply(HTTPStatus::CREATED, &headers, &receipt);
})?;

reply and reply_error consume the request, so a request is answered at most once, and Content-Length, Date and Server are generated. A reply copies what it sends, and since it consumes the request, a body made of the request’s bytes is assembled first, as receipt is above. A request owns its received bytes and is Send, so a handler may hand it to a worker thread and return, and the worker replies whenever it is done. A request dropped without a reply answers 500, as does one held by a handler that panics. A reply to a closed connection still consumes the request and reports Status::ConnectionClosed; any other failed reply keeps it, and dropping it then answers 500.

API

StructDetachedRequestA request taken off its polling thread, and the only owner of the verbs that may finish later: DetachedRequest::reply_parts, DetachedRequest::reply_stream and DetachedRequest::body_chunk.5
server.rs:1823 ↗
pub struct DetachedRequest(_);

It reads like the Request it came from, moves to any thread, and answers 500 when dropped unreplied. Its replies hand back a Deferred while a file or path part still sends, and waiting on one belongs off the polling thread, as that thread is the one sending it.

use std::sync::mpsc;
use ucall::*;

let (handed, received) = mpsc::channel::<DetachedRequest>();
let mut server = Server::new(&[Interface::new("127.0.0.1:0")])?;
server.get("/clip", move |request: Request| handed.send(request.detach()).unwrap())?;
let address = server.address(0).unwrap().to_owned();

// The worker outlives the scope, as the sender it waits on lives in the server's handler.
std::thread::spawn(move || {
    for detached in received {
        let parts = [ReplyPart::Bytes(b"frame".into())];
        if let Ok(Some(deferred)) = detached.reply_parts(HTTPStatus::OK, &[], &parts) {
            let _ = deferred.wait(); // or: deferred.await
        }
    }
});
let mut client = Client::new(ClientOptions { connections: 1, ..Default::default() })?;
std::thread::scope(|scope| {
    let polling = scope.spawn(|| server.take_calls(0));
    let response = client.get(format!("http://{address}/clip"));
    server.stop();
    polling.join().unwrap()?;
    assert_eq!(response?.body(), b"frame");
    Ok::<(), Error>(())
})?;

Implements

Deref
replyReplies with status, full header lines, and body, final on return, as Request::reply does.
server.rs:1837 ↗
pub fn reply(self, status: HTTPStatus, headers: &[BytesView<'_>], body: &[u8]) -> Result<()>
reply_errorReplies with an error status and a plain-text message, final on return, as Request::reply_error does.
server.rs:1843 ↗
pub fn reply_error(self, status: HTTPStatus, message: &str) -> Result<()>
reply_partsReplies with status, full header lines, and parts concatenated into the body, its Content-Length measured; None once final, and a Deferred while a file or path part still sends.
server.rs:1854 ↗
pub fn reply_parts<'f>(
    self,
    status: HTTPStatus,
    headers: &[BytesView<'_>],
    parts: &[ReplyPart<'_, 'f>],
) -> Result<Option<Deferred<'f>>>

A part failing before the head left replaces the reply with 404 for a missing, unreadable or irregular file, 416 for a range past its end, or 500, and the Deferred reports the cause. A refusal on return keeps the request, and dropping it then answers 500.

reply_streamSends the status line and header lines, and opens a body appended part by part: chunked, or of the length a Content-Length header declares.
server.rs:1880 ↗
pub fn reply_stream<'f>(
    self,
    status: HTTPStatus,
    headers: &[BytesView<'_>],
) -> Result<ReplyStream<'f>>

A closed connection still consumes the request and reports Status::ConnectionClosed; any other failure keeps it, and dropping it then answers 500.

body_chunkPulls the body bytes decoded since the last pull, waiting for them to arrive; empty once the body ended.
server.rs:1915 ↗
pub fn body_chunk(&mut self) -> Result<&[u8]>

On a BodyDelivery::Buffered endpoint the first pull is the whole body.

On the polling thread, where nothing can arrive while it runs, a pull finding nothing fails with Status::WouldBlock instead. A body cut short fails with Status::ConnectionClosed, and a malformed one with Status::Malformed.

StructPeerWhich interface a request arrived on, and who sent it.
server.rs:1353 ↗
pub struct Peer

Fields

network_interface_indexusizeThe position of its interface in the slice the server was started with.
static_keyNoiseKeyThe client's authenticated Noise static key, zeroed on plaintext interfaces.
addressPeerAddressThe socket address the connection was accepted from.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug
EnumPeerAddressThe socket address a peer connected from, as its connection was accepted.
server.rs:1364 ↗
pub enum PeerAddress

Values

UnknownThe engine reported no address the library knows.
Ip(SocketAddr)An IPv4 or IPv6 address and port, with the IPv6 scope.
UnixAn AF_UNIX peer, which binds no path.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug
EnumReplyPartOne part of a reply body, laid out as ucall_reply_part_t, so a slice of parts is the C array: bytes and paths borrow for 'b, copied before the call returns, and descriptors for 'f, read until the reply is final.
server.rs:1738 ↗
pub enum ReplyPart<'b, 'f>

Values

Bytesucall_reply_part_bytes_kBytes in memory, copied before the call returns.
Fileucall_reply_part_file_kA range of a regular file opened with O_DIRECT, which the event loop reads unbuffered after the call returns, so it stays open until the reply is final.
Pathucall_reply_part_path_kA range of a file the event loop opens beneath one of the interface's Interface::reply_directories, and closes once it is sent.

Implements

Clone, Copy, Debug, From<&'b IoSlice<'_>>
StructRequestOne received HTTP request, retained until a reply consumes it.16
server.rs:1429 ↗
pub struct Request

It owns its received bytes, so it may move to any thread and be replied to there after its handler returned. Dropping it unreplied answers 500. Reads the library refuses, which happens only on a thread whose CPU cannot run the server's class, read as empty.

use std::sync::mpsc;
use ucall::*;

let (handed, received) = mpsc::channel::<Request>();
let mut server = Server::new(&[Interface::new("127.0.0.1:0")])?;
server.get("/users/{name}", move |request: Request| handed.send(request).unwrap())?;
let address = server.address(0).unwrap().to_owned();

// The worker outlives the scope, as the sender it waits on lives in the server's handler.
std::thread::spawn(move || {
    for request in received {
        let name = request.path_parameter("name").unwrap_or_default();
        let greeting = [b"Hello, ", name].concat();
        let _ = request.reply(HTTPStatus::OK, &[], &greeting);
    }
});
let mut client = Client::new(ClientOptions { connections: 1, ..Default::default() })?;
std::thread::scope(|scope| {
    let polling = scope.spawn(|| server.take_calls(0));
    let response = client.get(format!("http://{address}/users/ada?verbose=1"));
    server.stop();
    polling.join().unwrap()?;
    assert_eq!(response?.body(), b"Hello, ada");
    Ok::<(), Error>(())
})?;

Implements

Send, Drop
verbThe HTTP method, which tells apart the requests of a Verb::Any endpoint.
server.rs:1439 ↗
pub fn verb(&self) -> Verb
pathThe request path as sent, still percent-encoded, without the query.
server.rs:1444 ↗
pub fn path(&self) -> &[u8]
path_parameterThe value the endpoint's template captured under name, case-sensitive.
server.rs:1477 ↗
pub fn path_parameter(&self, name: &str) -> Option<&[u8]>
queryThe value of query parameter name, case-sensitive and still percent-encoded.
server.rs:1482 ↗
pub fn query(&self, name: &str) -> Option<&[u8]>
fieldsEvery header field line in arrival order, repeated names apart: each name lowercased as the parser keeps it, and its value trimmed.
server.rs:1493 ↗
pub fn fields(&self) -> impl Iterator<Item = (&[u8], &[u8])> + '_
query_stringThe query after the first ?, still percent-encoded, or empty without one.
server.rs:1522 ↗
pub fn query_string(&self) -> &[u8]
bodyThe request body, decoded from chunked framing if it arrived that way.
server.rs:1536 ↗
pub fn body(&self) -> &[u8]
peerThe interface the request arrived on and the sender's authenticated static key, readable after its connection closed.
server.rs:1551 ↗
pub fn peer(&self) -> Peer
path_segments_countPath components in order, as sent and still percent-encoded.
server.rs:1573 ↗
pub fn path_segments_count(&self) -> usize
path_segmentPath component index, counting from zero, or None past the last one.
server.rs:1578 ↗
pub fn path_segment(&self, index: usize) -> Option<&[u8]>
parts_countParts in a multipart/form-data body, zero for any other body.
server.rs:1593 ↗
pub fn parts_count(&self) -> usize
partPart index of a multipart/form-data body, its payload undecoded.
server.rs:1601 ↗
pub fn part(&self, index: usize) -> Result<RequestPart<'_>>

Fails with Status::InvalidArgument past the last part, and with Status::Malformed when the body is no well-formed multipart body.

detachHands the request to a thread that may wait, where it gains the replies that may finish later; the polling thread itself must never wait on them.
server.rs:1625 ↗
pub fn detach(self) -> DetachedRequest
replyReplies with status, full header lines like "content-type: text/plain", and body.
server.rs:1678 ↗
pub fn reply(self, status: HTTPStatus, headers: &[BytesView<'_>], body: &[u8]) -> Result<()>

Content-Length, Date and Server are generated. A closed connection still consumes the request and reports Status::ConnectionClosed; any other failure keeps it, and dropping it then answers 500.

reply_errorReplies with an error status, 400 or above, and a plain-text message.
server.rs:1694 ↗
pub fn reply_error(self, status: HTTPStatus, message: &str) -> Result<()>
StructRequestPartOne part of a multipart/form-data request body, borrowing the received bytes.
server.rs:1724 ↗
pub struct RequestPart<'request>

Fields

name&'request [u8]The name its Content-Disposition gives.
content_type&'request [u8]Its Content-Type, empty when absent.
data&'request [u8]Its payload, undecoded.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug
StructBytesViewA borrowed header line, laid out so a slice of them is lent to the library without copying.1
types.rs:541 ↗
pub struct BytesView<'bytes>

Implements

Clone, Copy, From<&'bytes [u8]>, From<&'bytes str>, From<&'bytes [u8; LENGTH]>, Debug
as_bytesThe bytes this view borrows.
types.rs:569 ↗
pub fn as_bytes(&self) -> &'bytes [u8]
StructDeferredA reply still sending a file or path part: it owns the outcome the event loop makes final, and the descriptors its parts borrow stay open for 'f, until it is final.2
types.rs:301 ↗
pub struct Deferred<'f>

Deferred::status polls without blocking, Deferred::wait blocks until the reply is final, and .await polls it from async code. Dropping one that is not final waits for it, so it belongs off the polling thread, which is the one finishing it.

Implements

Send, Drop, Debug, IntoFuture
statusErr(Status::Deferred) until the reply is final, then its final status, read without blocking through ucall_outcome_status.
types.rs:326 ↗
pub fn status(&self) -> Result<(), Status>
waitBlocks until the reply is final, through ucall_outcome_wait, and reports how it ended.
types.rs:331 ↗
pub fn wait(self) -> Result<()>
StructHTTPStatusAn HTTP status code, for replies and received responses alike; codes without a constant here are just as valid.23
types.rs:588 ↗
pub struct HTTPStatus(pub u32);

Fields

0u32

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, PartialOrd, Ord, Debug, Hash
OK200, the request succeeded.
types.rs:592 ↗
pub const OK: HTTPStatus = _;
CREATED201, a resource was created.
types.rs:594 ↗
pub const CREATED: HTTPStatus = _;
NO_CONTENT204, success with an empty body.
types.rs:596 ↗
pub const NO_CONTENT: HTTPStatus = _;
PARTIAL_CONTENT206, success with the requested range of the body.
types.rs:598 ↗
pub const PARTIAL_CONTENT: HTTPStatus = _;
MOVED_PERMANENTLY301, the resource has a new permanent URL.
types.rs:600 ↗
pub const MOVED_PERMANENTLY: HTTPStatus = _;
FOUND302, the resource is temporarily at a different URL.
types.rs:602 ↗
pub const FOUND: HTTPStatus = _;
NOT_MODIFIED304, the cached copy is still fresh.
types.rs:604 ↗
pub const NOT_MODIFIED: HTTPStatus = _;
BAD_REQUEST400, the request is malformed.
types.rs:606 ↗
pub const BAD_REQUEST: HTTPStatus = _;
UNAUTHORIZED401, authentication is required or failed.
types.rs:608 ↗
pub const UNAUTHORIZED: HTTPStatus = _;
FORBIDDEN403, authenticated but not permitted.
types.rs:610 ↗
pub const FORBIDDEN: HTTPStatus = _;
NOT_FOUND404, no endpoint matches the path.
types.rs:612 ↗
pub const NOT_FOUND: HTTPStatus = _;
METHOD_NOT_ALLOWED405, the endpoint exists, but not for this verb.
types.rs:614 ↗
pub const METHOD_NOT_ALLOWED: HTTPStatus = _;
TIMEOUT408, the client took too long to send the request.
types.rs:616 ↗
pub const TIMEOUT: HTTPStatus = _;
CONFLICT409, the request conflicts with the resource's state.
types.rs:618 ↗
pub const CONFLICT: HTTPStatus = _;
PRECONDITION_FAILED412, a request condition does not match the resource.
types.rs:620 ↗
pub const PRECONDITION_FAILED: HTTPStatus = _;
PAYLOAD_TOO_LARGE413, the request body is too large.
types.rs:622 ↗
pub const PAYLOAD_TOO_LARGE: HTTPStatus = _;
TOO_MANY_REQUESTS429, the rate limit is exceeded.
types.rs:624 ↗
pub const TOO_MANY_REQUESTS: HTTPStatus = _;
INTERNAL_ERROR500, a handler failed or dropped its request unanswered.
types.rs:626 ↗
pub const INTERNAL_ERROR: HTTPStatus = _;
NOT_IMPLEMENTED501, the verb or feature is not supported.
types.rs:628 ↗
pub const NOT_IMPLEMENTED: HTTPStatus = _;
BAD_GATEWAY502, an upstream answered with something no HTTP peer would send.
types.rs:630 ↗
pub const BAD_GATEWAY: HTTPStatus = _;
SERVICE_UNAVAILABLE503, the server is overloaded, like a batch endpoint whose queue is full.
types.rs:632 ↗
pub const SERVICE_UNAVAILABLE: HTTPStatus = _;
GATEWAY_TIMEOUT504, an upstream dependency timed out.
types.rs:634 ↗
pub const GATEWAY_TIMEOUT: HTTPStatus = _;
VERSION_NOT_SUPPORTED505, the request names an HTTP version the server does not speak.
types.rs:636 ↗
pub const VERSION_NOT_SUPPORTED: HTTPStatus = _;
EnumPercentModeWhich part of a URL decode_percent reads, as only a query spells a space +.
types.rs:431 ↗
pub enum PercentMode

Values

PathA path, where + stays itself.
QueryA query, where + is a space.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Hash
StructWaitA Deferred being awaited: every poll reads its status, and one not yet final asks to be polled again.
types.rs:368 ↗
pub struct Wait<'f>(_);

The library has no wake-up, so an executor re-polls it while other tasks run, spinning only when nothing else is ready; Deferred::wait blocks on the CPU class's waiter.

Implements

Future

Functions

decode_percentDecodes the %XX escapes of encoded, like a Request::query value, and + as a space in PercentMode::Query.
lib.rs:450 ↗
pub fn decode_percent(encoded: &[u8], mode: PercentMode) -> Result<Vec<u8>>

Fails with Status::InvalidArgument on a % without two hexadecimal digits.

use ucall::{decode_percent, PercentMode};

assert_eq!(decode_percent(b"caf%C3%A9+au+lait", PercentMode::Query)?, "café au lait".as_bytes());
assert!(decode_percent(b"100%", PercentMode::Path).is_err());