Send HTTP requests and decode common response bodies.
Requests are built and read with the shared
roc-lang/httpRequest and Response
types. This module adds the hosted effects plus JSON and UTF-8 conveniences.
An app that names Request or Response declares the http package in its
own header, beside the platform:
Http.Client.get! and Http.Client.get_utf8! take a URL and return decoded data, so
an app that uses only those needs no package dependency of its own.
Http.Client.send! waits, so it belongs inside Task.spawn!:
update! = |model, input, io| {
if input.devices.key_pressed(KeyR) {
http = io.http()
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)
}
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 has a deadline and a hard limit 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. A response over the cap fails
rather than being truncated.
An HTTP status is not an error. A 404 or a 503 arrives as Ok(response)
with that status. Only a request that gets no complete response is HttpErr.
get! and send_json! infer the JSON type from their call site. Add a type
annotation when the expected decoded or encoded type is ambiguous.
HTTPS verifies peers with the operating system's certificate store. Custom
certificate authorities and disabling verification are not supported.
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.
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.
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 holds 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.
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 returns 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!.
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 returns 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!.