A program asks other servers for things with an HttpRequest - most easily written as #HTTP {{{ ... }}}, the way the request goes over the wire - and sends it through &http. The answer is a Response, the same type a server's routes answer with, once the server has given it - so sending is asynchronous, and a request is sent from an @async function that awaits it.
send() gives a Promise<IOError ! Response>: the Response - whatever its status, a 404 or a 500 too - once the server has answered, or an IOError when no answer came: no network, a refused connection, a name that doesn't resolve. response.ok() says whether the status is a success (200 to 299). The headers of a Response a server gave are named in lower case, and its body is a String when its Content-Type is text (text/..., JSON, XML, JavaScript, a form - or none), Bytes otherwise.
bodyText() gives the body as text - Bytes read as UTF-8, a ParseError when they aren't - and bodyJson() reads it as JSON: an object is a Map<String, Unknown> of its members, an array an Array<Unknown>, a number an F64, null undefined, and anything that isn't JSON a ParseError saying where.
The first line is the method and the URL - or the URL alone, for a GET - with HTTP/1.1 after them if you like. Each line after it, up to the first blank one, is a header, Name: value; what follows the blank line is the body. The text is checked when the program is compiled: a method in capitals, a URL starting with http://, https://, / or a placeholder, a header on every line before the body.
A #{ } placeholder goes in the way that's safe where it is. In the URL's path or query its text is percent-encoded, so a /, ?, & or space in it can't change where the request goes; before the path - #{ base }/todos - it's written as it is. In a header it's text, a line break in it a space, so it can't add a header of its own. A placeholder that's the whole body is the body itself: Bytes are sent as they are, Json as its JSON text, anything else as its text.
HttpRequest is an ordinary struct too, for a request put together some other way: HttpRequest{method ≔ "DELETE", url ≔ address}.
A request's timeout gives up on it - an IOError, "no answer within 5000 ms" - when the server hasn't answered in time; there's no limit unless one's set. A redirect (a 3xx answer with a Location) is followed to where it points, unless followRedirects is false: then the 3xx Response is the answer. A web page's browser always follows them.
A request goes out as soon as send() is called, and the program goes on - only an await waits for it. Requests sent before the first await are on their way together. For a list of them, Promise.all gives every result once the last has come - each made a value of its own first, as statusOf does, so a failed request is still in the list.
&http is a context value, so a test injects its own: #builtin.implementation.MemoryHttpClient.new(answers) answers from a Map, keyed by "METHOD url", at once and with nothing sent anywhere - a request it has no answer for gets a 404 - and requests() gives every request it was sent, to check what the code asked for. The test beside the first example (its todos.test.rfn tab) does both.
A request gets one answer. A WebSocket stays open, and messages go both ways in any order: WebSocket.connect(url) opens one to a ws:// or wss:// server, send sends text or Bytes, and receive waits for the next message - a String for text, Bytes for binary, undefined once the server closes the connection. Waiting asks &cancellation in between. Sockets need Bun's or Node's host.
The builtin &http is the JavaScript host's - --js, which --run and --test use. On Bun the requests go out on a thread of their own, and the program waits for their answers whenever it has nothing else to do. A request to a server the program itself is running (its own @serve) is an IOError straight away: the server couldn't answer while the program waits. Without a JavaScript host - a plain WASI program, or a native executable - there's no client, and every request is an IOError saying so.
A page never waits: send() hands the request to the browser and the program carries on - the click's handler above shows "asking..." at once. When the answer comes, the @async function goes on from its await, and an app's components it changed are rendered again. An @async @main ends at its first await on a request, the rest of it running once the answer's there; promise.block() can't wait for one, and panics saying so. A URL starting with / is the page's own server; a server on another origin has to allow the page to ask it (CORS). In a browser's worker - the docs' playground runs examples in one - a request is waited for there and then, as on Bun.