Kite

http

std/http — talking to a server, and answering one.

The two halves are not symmetric. A client runs in a sandbox that already has fetch and EventSource, so the client half is a declared boundary over those and everything else is Kite. A server runs somewhere with sockets, which no Kite target has yet — so the server half here is the part that needs no sockets: the types, the router, and handlers that can be called directly and tested without a network.

A 404 is a Response, not an error: the request succeeded and the answer was "no". Only a transport failure is an error.

Any method the host will send, this will send — fetch forbids only CONNECT, TRACE and TRACK — so QUERY works here the day a server answers it, with no change to this module.

Request

pub struct Request

Response

pub struct Response

ok

pub fn ok(body: str) -> Response

not_found

pub fn not_found() -> Response

status

pub fn status(code: int, body: str) -> Response

succeeded

pub fn succeeded(r: Response) -> bool

Whether the status is in the 2xx range.

pub fn header(r: Response, name: str) -> str

A header of a response that came from the host, answered as fetch's own headers.get answers it: the name is compared ignoring ASCII case, and a header that arrived more than once answers with every value, in order, joined by , . A header the response did not carry answers with an empty string, and so does every header of a response built in Kite, which carries none.

Credentials

pub enum Credentials

Whether cookies, TLS client certificates and HTTP auth go out with a request, and whether a Set-Cookie coming back is kept.

This is the one fetch option a program cannot work around: an app that signs in with a cookie sends one on every request, and there is no header a caller can set instead — the browser will not let a page write Cookie.

Redirect

pub enum Redirect

What to do when the answer is a redirect.

Options

pub struct Options

Everything about a request except its method, its URL and its body.

A struct rather than six more parameters, which is what the specification says to do with many optional inputs — and it means the next option added here does not change any call site that did not want it.

mode is deliberately absent. Its only interesting value is no-cors, which yields a response whose status is 0 and whose body cannot be read: the request goes out and the program is told nothing. A boundary that can only be used to lose information is not worth the word.

sending

pub fn sending() -> Options

The defaults, to be changed where a caller cares:

let opts = http.Options{ ..http.sending(), credentials: http.Credentials.Include }
let (res, err) = await http.send_with("POST", url, body, opts)

sending_with

pub fn sending_with(headers: str) -> Options

The same, carrying the caller's headers as name: value lines.

Not for a value that came from outside the program. The lines are split on the newline on the way to fetch, so a newline inside a value stops being data and starts a header of its own: "x-user: \(name)", with a name someone else chose, is one newline away from sending whatever Authorization they wrote. That cannot be told from two headers the program meant — the joined text is all there is — so it is not refused here, and [sending_pairs] is the form to use instead. What can be told is refused when the request is sent: a line that is not name: value, or a carriage return inside one.

sending_pairs

pub fn sending_pairs(headers: [Header]) -> (Options, error)

The same, with headers given as pairs — the form to use whenever a value came from somewhere else.

Joined here from parts that arrive separately, which is what makes "the program meant two headers" and "a value added one" decidable, as it is for [respond]. A name that is not an HTTP token, or a value carrying a line break, is refused rather than stripped: there is no escaping that keeps the meaning.

let (opts, err) = http.sending_pairs([http.Header{ name: "x-user", value: name }])
check err
let (res, rerr) = await http.send_with("GET", url, "", opts)

get

pub async fn get(url: str) -> (Response, error)

post

pub async fn post(url: str, body: str) -> (Response, error)

put

pub async fn put(url: str, body: str) -> (Response, error)

delete

pub async fn delete(url: str) -> (Response, error)

patch

pub async fn patch(url: str, body: str) -> (Response, error)
pub async fn head(url: str) -> (Response, error)

A HEAD request. The status and the headers arrive; the body is empty by definition, not by accident.

options

pub async fn options(url: str) -> (Response, error)

query

pub async fn query(url: str, body: str) -> (Response, error)

A QUERY request: a read that carries a body.

QUERY is what GET would be if a request could ask a long question — safe, idempotent and cacheable, with the question in the body instead of squeezed into a URL and its length limits. It is still a draft (draft-ietf-httpbis-safe-method-w-body) rather than a finished RFC.

It works today because fetch forbids exactly three methods — CONNECT, TRACE and TRACK — and passes every other token through untouched. Two things follow, and both are the server's business rather than this module's: QUERY is not CORS-safelisted, so a cross-origin call is preceded by a preflight OPTIONS; and a server that does not know the method answers 405 rather than failing, which is a Response here and not an error.

The body is sent as JSON. A different query language — SQL, GraphQL, anything — goes through send with the content type it wants.

send

pub async fn send(method: str, url: str, body: str, headers: str) -> (Response, error)

A request with headers and nothing else to say about it — which is most of them, and is what get, post and the rest are written in terms of.

send_with

pub async fn send_with(
    method: str,
    url: str,
    body: str,
    options: Options,
) -> (Response, error)

The same, saying what the request carries besides its body.

The one every other function here goes through. Waiting is task.wait_host(): what is being waited for is not another task but the host itself, and saying so is what lets the runtime hand the event loop back rather than spin.

Events

pub struct Events

Event

pub struct Event

events

pub async fn events(url: str) -> (Events, error)

Open a stream of server-sent events, waiting until it is open.

GET only, and no headers: that is EventSource, not a choice made here. A stream that needs an Authorization header cannot be opened this way — a cookie the browser already sends, or a token in the URL, is what is left.

events_named

pub async fn events_named(url: str, names: str) -> (Events, error)

The same, also delivering events whose event: field is one of names, given one per line.

EventSource hands a named event to a listener registered for that name and offers no way to ask for all of them, so a program that wants event: tick says tick here. Naming them at open is what keeps the first one from arriving before anything is listening.

listen

pub fn listen(stream: Events, name: str)

Deliver events named name from now on.

For a name discovered while the stream is running. Anything known before it opened belongs in events_named, which cannot miss an event this may.

receive

pub async fn receive(stream: Events) -> (Event, error)

The next event, waiting for one to arrive.

Waiting is task.wait_host() for the reason a request waits that way: what the task waits for is the host's event loop, and a scheduler told so hands the loop back rather than spinning. A stream that is merely quiet is not a stalled program, and this is what tells the two apart.

pending

pub fn pending(stream: Events) -> int

How many events are waiting. A program with something else to do asks this rather than receive.

close

pub fn close(stream: Events)

Stop the stream, and stop it reconnecting.

Events that already arrived stay readable: receive drains what is waiting and only then reports the stream closed. Discarding them would throw away data the host had already delivered, which is a loss a program has no way to notice.

Route

pub struct Route

route

pub fn route(method: str, pattern: str, handle: fn(Request) -> Response) -> Route

serve

pub fn serve(routes: [Route], request: Request) -> Response

The first route that matches, or a 404.

Matched on the path alone: a query string or a fragment after it takes no part, so /?utm=1 is / and /users/7?tab=posts is /users/:id. Read the query with [query_parameter].

matches

pub fn matches(pattern: str, path: str) -> bool

Whether a path matches a pattern, where :name stands for one segment.

path may be a whole request target: whatever follows a ? or a # is not part of the path, and is not compared. The server hands a handler the target as the client sent it, query and all, and routing on that made a link with ?utm=1 on it a 404.

parameter

pub fn parameter(pattern: str, path: str, name: str) -> Option<str>

The value a :name segment captured, or nil.

As it appears in the path — still percent-encoded, so /users/a%20b captures a%20b — and without the query: /users/7?tab=posts captures 7, where it used to capture 7?tab=posts.

query_parameter

pub fn query_parameter(r: Request, name: str) -> Option<str>

The value of a query-string parameter, or nil when the request has none by that name.

Decoded as the URL Standard decodes application/x-www-form-urlencoded, which is what a form and URLSearchParams send: + is a space, %XX is a byte, and the bytes are read as UTF-8, with U+FFFD for a sequence that is not. The name is decoded the same way before it is compared. A parameter written with no = has the value "", and when a name repeats the first one answers.

let r = Request{ method: "GET", path: "/search?q=kite+lang&page=2", body: "", headers: "" }
assert(or_else(query_parameter(r, "q"), "") == "kite lang", "a plus is a space")
assert(or_else(query_parameter(r, "page"), "") == "2", "the second parameter")
assert(query_parameter(r, "sort") == nil, "one that is not there")

request_header

pub fn request_header(request: Request, name: str) -> Option<str>

A header's value out of the name: value lines a request carries.

Server

pub struct Server

Something listening on a port.

Incoming

pub struct Incoming

A request that arrived, and the handle to answer it with.

The handle is kept beside the Request rather than inside it, so a Request stays a value a test can write out by hand. A handler is a function from a request to a response, and that is only true if a request is something anyone can make.

open

pub async fn open(port: int) -> (Server, error)

Start listening. Port 0 asks the host to choose one; port_of says which.

It is async because binding a socket is not instant, and a server that answered "which port?" before the host had one would answer wrongly rather than late. Waiting is task.wait_host() for the reason everything in this module waits that way: what is being waited for is the host.

port_of

pub fn port_of(server: Server) -> int

The port a server is actually listening on.

accept

pub async fn accept(server: Server) -> (Incoming, error)

Wait for the next request.

Waiting is task.wait_host() for the reason a fetch waits that way: what is being waited for is the host rather than another task, and saying so is what lets the runtime hand the event loop back instead of spinning through a queue that cannot fill while it holds the thread.

A server that has been [shut] with nothing waiting answers with an error rather than waiting: nothing will ever arrive, and a loop around this used to hang there for good. Requests that arrived before the shut are still handed out first.

Header

pub struct Header

One response header, as a name and a value that are still two things.

Written as a literal — http.Header{ name: "location", value: "/x" } — rather than through a constructor, because header already means "read a header off a response" in this module and one name cannot mean two things.

respond

pub fn respond(incoming: Incoming, response: Response, headers: [Header]) -> error

Answer a request.

Headers are given as pairs, not as text, and that is the whole of the design. Everything else crossing this boundary is name: value lines, and for a response that encoding is unsafe: the host splits on the newline, so a newline inside a value stops being data and becomes a separator. A handler that put anything a request said into a header — a path, an identifier, a filename — would be one newline away from letting the caller add a Set-Cookie of their own.

Joining the lines here, from parts the library was given separately, is what makes the difference between "the program meant two headers" and "somebody else added one" decidable. It cannot be decided from the joined string, and the host is handed nothing else.

A name or value carrying a carriage return or a newline is refused rather than stripped: there is no escaping that keeps the meaning, and a header nobody can read is better than one somebody else wrote.

run

pub async fn run(server: Server, routes: [Route]) -> (int, error)

Accept requests and answer them from a routing table, until the server is closed or the host fails.

This is the whole of a server: serve already turns a table and a request into a response, and it did so before anything could listen — which is why a handler was testable by calling it long before this existed, and stays that way.

Shutting the server is how this ends well: once it is shut and every request that arrived before has been answered, the count of answered requests comes back with no error.

serve_closed

pub fn serve_closed(server: Server) -> bool

Whether a server has stopped listening.

shut

pub fn shut(server: Server)

Stop listening. Requests already taken can still be answered; nothing new arrives.

Named shut rather than close because close already means "stop listening to this event stream" in this module, and Kite has no overloading: one name, one signature. Two things that are not the same thing do not get the same name.