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.
RequestResponseoknot_foundstatussucceededheaderCredentialsRedirectOptionssendingsending_withgetpostputdeletepatchheadoptionsquerysendsend_withEventsEventeventsevents_namedlistenreceivependingcloseRouterouteservematchesparameterrequest_headerServerIncomingopenport_ofacceptHeaderrespondrunserve_closedshut
Request
struct Request
pub method: strpub path: strpub body: strpub headers: str—name: valuepairs, one per line — the shape that crosses the host boundary, since only text does.
Response
struct Response
pub status: intpub body: strid: int— The handle the response came from, for reading a header back.
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.
header
pub fn header(r: Response, name: str) -> str
A header of a response that came from the host. A response built in Kite carries no headers, and answers with an empty string.
Credentials
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.
Omit— Never. A request that is purely public data.SameOrigin— To the origin the page was served from, and nowhere else. Whatfetchdoes when nobody says.Include— Cross-origin too. The server has to answer withAccess-Control-Allow-Credentials: trueand name a single origin —*is refused by the browser precisely here.
Redirect
enum Redirect
What to do when the answer is a redirect.
Follow— Follow it, up to the browser's own limit. The default.Error— Treat it as a transport failure, which arrives here as anerror.Manual— Hand back the redirect itself, unfollowed and opaque.
Options
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.
pub headers: str—name: valuepairs, one per line.pub credentials: Credentialspub redirect: Redirect
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.
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)
head
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
struct Events
id: int
Event
struct Event
pub name: str— Theevent:field, or "message" when the event had none.pub id: str— Theid:field, or "" when the event had none. The host replays it asLast-Event-IDafter a reconnection, so a program rarely needs it.pub data: str
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
struct Route
pub method: strpub pattern: str— A path, with:namestanding for one segment.pub handle: fn(Request) -> Response
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.
matches
pub fn matches(pattern: str, path: str) -> bool
Whether a path matches a pattern, where :name stands for one segment.
parameter
pub fn parameter(pattern: str, path: str, name: str) -> Option<str>
The value a :name segment captured, or nil.
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
struct Server
Something listening on a port.
id: int
Incoming
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.
pub request: Requestid: int
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.
Header
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.
pub name: strpub value: str
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.
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.