An HTTP client for live dashboards and remote data visualization.
Requests are built and read with the shared
roc-lang/httpRequest and Response
types, exactly as in
basic-cli.
This module adds the effects and the small JSON and UTF-8 conveniences.
An app that names Request or Response declares the http package in its
own header, beside the platform:
The platform entry above is the path the repository's own examples use;
outside the checkout it is the platform declaration from the latest
release. The http line is the version this platform is built against, and
an app that names Request or Response must declare that same one.
Http.get! and Http.get_utf8! take a URL and hand back decoded data, so
an app that uses only those needs no package dependency of its own.
Http.send! waits, so it belongs inside Task.spawn!:
update! = |model, input| {
if input.devices.key_pressed(KeyR) {
Task.spawn!(
input,
|| match Http.get_utf8!("http://127.0.0.1:8000/data.json") {
Ok(body) => Loaded(body)
Err(InvalidUrl(_)) => Failed("that is not a URL this platform will fetch")
Err(HttpErr(Timeout)) => Failed("the request timed out")
Err(HttpErr(_)) => Failed("the request failed at the network layer")
Err(BadBody(_)) => Failed("the reply was not valid UTF-8")
Err(_) => Failed("the request failed")
},
)
}
Ok(model)
}
The task parks on the host's socket while the frame loop keeps drawing, and
the closure's return value arrives on a later Input.messages. send! is
legal in init!, where it blocks startup, and in tasks, where it parks the
task; it is refused in update! and render!.
Up to three redirects are followed, and the Response is the one at the end
of that chain.
Every send carries a deadline and a hard cap on the response body, taken
from Http.default_config: thirty seconds, and eight megabytes. Pass a
Config to send_with! for different ones, and 0 in either field to
disable that limit. A request that sets TimeoutMilliseconds itself
overrides the config's deadline; NoTimeout on a request means the config's
deadline applies, so an ordinarily built request is never sent without one.
The body cap is measured after decompression, and it is a refusal rather
than a truncation: a response over the cap fails the send, because a
truncated body decodes into wrong data rather than into an error. What it
bounds is how much of this process's memory a remote server can choose to
use.
An HTTP status is not an error. A 404 or a 503 arrives as Ok(response)
carrying that status; only a failure to complete the exchange is HttpErr.
get! and send_json! have a _ where the decoded or encoded type would
be. That is static dispatch: the JSON parser and encoder are chosen from the
type the call site expects, so the same get! answers a List(Reading) in
one place and a { name : Str } in another, and there is no decoder to pass
in. Annotate the binding, or the field the value goes into, and the
inference does the rest.
TLS: https URLs are served by Zig's std.crypto.tls against the operating
system's certificate store -- on Linux the usual /etc/ssl bundle. Nothing
here configures a custom certificate authority or turns verification off. A
store that cannot be loaded fails the send with HttpErr(Other(...)) saying
so, rather than continuing unverified.
Thirty seconds, and eight megabytes of response body.
The timeout is generous enough for a slow dashboard endpoint and short
enough that a task waiting on a dead server is eventually collected. The
body cap holds a large JSON payload but not an accidental video.
Validate and send an HTTP request under default_config.
The request URI must be an absolute HTTP or HTTPS URL accepted by Url.
An invalid URL answers InvalidUrl before any host effect occurs.
Fragments are removed, because they are client-side identifiers and are
not sent.
The method must be one of the nine RFC methods. QUERY and any
Unknown(ext) method fail as HttpErr(Other(...)) naming the method,
also before any host effect occurs: the host's method type has no
representation for either, and reporting it as Other keeps an app's
exhaustive match over TransportErr from breaking over a request no
host could send.
Legal in init!, where it blocks startup, and in tasks, where it parks
the task; refused in update! and render!.
Encode a value as JSON and set it as the request body.
This uses Roc's builtin JSON encoder, so the value's type determines the
encoder through static dispatch. A Content-Type: application/json
header is added.
Perform an HTTP GET and decode the response body as a UTF-8 Str.
The argument is a validated Url. Quoted literals work through
Url.from_quote, so a URL written out in the source is checked at compile
time; a string built at runtime goes through Url.parse.
A body that is not valid UTF-8 answers BadBody(Str). That is this
function's own decoding failure, and is not the transport's
MalformedResponse: the reply arrived and was a well-formed HTTP
response, it just is not text. The status is not inspected, so an error
page comes back as the Str the server sent.
Perform an HTTP GET and decode the response body as JSON.
The expected result type selects the parser through static dispatch. The
status is not inspected, so a JSON error page decodes if it happens to fit
the expected shape. Same phases as send!.
Why the exchange did not produce an HTTP response.
An HTTP status is never one of these: a 404 or a 503 is an Ok
response.
Timeout is the deadline expiring before the exchange finished.
NetworkError is a connection that could not be made or did not survive
the exchange -- a refused port, a dropped socket, an unreachable host.
MalformedResponse is a reply that arrived but was not a well-formed
HTTP response. Other carries the host's own description as UTF-8 bytes: a
name that would not resolve, a body over max_response_bytes, a
certificate store that could not be loaded, or a method this platform
cannot send.
timeout_ms is the deadline for the whole exchange: connect, send,
response head, and body. 0 means no deadline, which is only ever
appropriate against a server you control. A request that sets
TimeoutMilliseconds itself overrides this value.
max_response_bytes caps the decompressed response body, and 0 means
no cap. A response that would exceed it fails with Other rather than
being truncated, because a truncated body silently decodes into wrong
data.
A plain record; build one with
{ ..Http.default_config, timeout_ms: 5_000 } rather than a chain of
with_* calls.