HTTP requests
Use day-part-http to fetch data without blocking the interface. This guide starts with a
button that loads a response, then shows cancellation and requests that follow a page’s state.
The examples use day::task, which resumes futures on the UI thread so they can update signals.
The asynchronous API works on macOS, iOS, Android, Windows, Linux, HarmonyOS, and web.
Blocking requests are unavailable on web; unknown targets return Unsupported.
1. Fetch into UI state
Add the crate to Cargo.toml. Use the same Day revision as the rest of your app:
[dependencies]
day-part-http = { git = "https://github.com/daybrite/day.git" }
The button below starts a request and shows either the response or an error. Replace the
example URL with your own endpoint. Handle errors inside the task: day::task expects
Output = (), so the task cannot return a Result.
use day::prelude::*;
use day_part_http::{Request, fetch_future};
fn forecast_row() -> impl Piece {
let readout = Signal::new(String::new());
let loading = Signal::new(false);
column((
button("Fetch").action(move || {
loading.set(true);
day::task(async move {
match fetch_future(Request::get("https://api.example.com/data.json")).await {
Ok(resp) if (200..300).contains(&resp.status) => {
readout.set(resp.text().into_owned());
}
Ok(resp) => readout.set(format!("HTTP {}", resp.status)),
Err(e) => readout.set(format!("error: {e}")),
}
loading.set(false);
});
}),
label(move || if loading.get() { "loading…".to_string() } else { readout.get() }),
))
.spacing(8.0)
}
Check both the request result and the HTTP status:
- 4xx/5xx are
Ok. An HTTP error status is a response (resp.status == 404), not anHttpError, which is why the sample checks the status range. Errors are transport-level only:BadUrl,Timeout,Dns,Connect,Tls,Io,Cancelled,Unsupported. timeoutbounds progress, not the transfer. It covers connecting, awaiting the response head, and idle gaps; a long download that keeps moving is never cut off. Default 30 s.
Response is { status, headers, body } plus text() (lossy UTF-8) and a case-insensitive
header(name) lookup. There is no built-in JSON layer; parse resp.body with serde_json,
added as an app dependency:
#[derive(serde::Deserialize)]
struct Forecast { temperature: f64 }
let data: Result<Forecast, _> = serde_json::from_slice(&resp.body);
2. Cancel an in-flight request
Tasks are not owned by the scope that spawned them: leaving the page does not stop a running fetch.
To cancel, keep the TaskHandle that day::task returns and call .abort(). Aborting drops the
task’s future, and dropping the FetchFuture cancels the platform request:
NSURLSessionTask.cancel on Apple, OkHttp Call.cancel on Android, the fetch’s AbortController
on web. Windows and the Rust fallback can’t cancel mid-flight; they run the request out on a worker
thread and discard the result. Re-tapping below supersedes the previous request:
let inflight: Rc<Cell<Option<day::TaskHandle>>> = Rc::new(Cell::new(None));
button("Check").action(move || {
if let Some(prev) = inflight.take() {
prev.abort(); // drops the FetchFuture → platform cancel
}
let slot = inflight.clone();
let handle = day::task(async move {
match fetch_future(Request::get(url.get_untracked())).await {
Ok(resp) => readout.set(format!("HTTP {} · {} bytes", resp.status, resp.body.len())),
Err(e) => readout.set(format!("error: {e}")),
}
slot.set(None);
});
inflight.set(Some(handle));
});
Aborting a finished task is a no-op, and a write to a signal whose scope has since been disposed is a silent no-op, so a late completion can’t crash a page the user already left.
3. Load on mount with Resource
Use day::reactive::Resource when a page should load data on arrival and reload when an input
changes. In this example, city is a Signal<String>. Each change starts a new fetch, and
forecast holds the loading, success, or failure state. The fetcher returns a Result;
Resource turns an error into Load::Failed:
use day::reactive::{Load, Resource};
let forecast: Resource<String> = Resource::new(
move || city.get(), // tracked — a city change refetches
|city| async move {
let url = format!("https://api.example.com/wx?city={city}");
let resp = day_part_http::fetch_future(Request::get(url)).await?;
Ok::<_, day_part_http::HttpError>(resp.text().into_owned())
},
);
label(move || forecast.with(|l| match l {
Load::Loading => "…".to_string(),
Load::Ready(s) => s.clone(),
Load::Failed(e) => format!("error: {e}"),
}))
The latest source value wins. A source change aborts the in-flight fetch (dropping its
FetchFuture, the same cancel path as above) and a stale completion writes nothing. Scope
disposal aborts it too, with no handle bookkeeping, and forecast.refetch() forces a fresh
fetch. Watch the import: the prelude’s Resource is the bundled-asset handle; the async one is
day::reactive::Resource.
4. Other methods, headers, and bodies
Request builds every common shape: get/delete/head(url), and post/put/patch(url, body) taking the body bytes up front. .header(name, value) appends (duplicates allowed,
sent in order), and .allow_expensive(bool) / .allow_constrained(bool) gate cellular and
Low Data Mode use (native on Apple, advisory elsewhere):
let req = Request::post("https://api.example.com/notes", note_json_bytes)
.header("Content-Type", "application/json")
.timeout(std::time::Duration::from_secs(15));
For large downloads, fetch_to_file(&req, &dest) streams the body straight to disk, never
holding it in memory; fetch_streamed adds per-chunk control (progress, hashing, mid-body
cancel). To cache a response across launches, write resp.body with day-part-fs; see
Local storage.
Platform behavior
Apple targets use NSURLSession, Android uses OkHttp, Windows uses WinHTTP, and the web uses
fetch(). These implementations use platform networking settings, such as proxy configuration,
VPN routing, certificate stores, and Low Data Mode where supported.
Linux and HarmonyOS use a bundled ureq + rustls client, with proxy support limited to the
http_proxy environment variables.
day_part_http::tier() reports the compiled target’s support tier.
Pitfalls
- Don’t block the UI thread.
fetch,fetch_to_file, andfetch_streamedblock their calling thread; run them on a worker thread, or use the futures underday::task. On web the blocking calls returnUnsupported; the single browser thread cannot wait. fetch_asynccompletes on a background thread. Never touch signals directly in its callback; capture aSetter(signal.setter()), which hops to the UI thread itself. Underday::task,fetch_futureresumes on the UI thread and needs none of this.- Cleartext
http://is platform policy. Apple’s ATS refuses non-HTTPS URLs without a scoped Info.plist exception (loopback is exempt); Android blocks cleartext app-wide since targetSdk 28, loopback included; scope an exception innetwork_security_config.xml. The Rust fallback enforces no such policy, another reasontier()exists. - The browser adds CORS. On web, cross-origin requests need the server’s opt-in, and
browser-controlled request headers (
Host,Cookie,Origin) are ignored per the fetch spec. Network-level failures surface asHttpError::Io, since browsers hide DNS/TLS detail.
Reference
http — the full Request/Response contract, per-platform
realization, error mapping, and the cancel matrix.
async — day::task, TaskHandle, Resource, and task lifetimes and main-thread updates.