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 an HttpError, which is why the sample checks the status range. Errors are transport-level only: BadUrl, Timeout, Dns, Connect, Tls, Io, Cancelled, Unsupported.
  • timeout bounds 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, and fetch_streamed block their calling thread; run them on a worker thread, or use the futures under day::task. On web the blocking calls return Unsupported; the single browser thread cannot wait.
  • fetch_async completes on a background thread. Never touch signals directly in its callback; capture a Setter (signal.setter()), which hops to the UI thread itself. Under day::task, fetch_future resumes 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 in network_security_config.xml. The Rust fallback enforces no such policy, another reason tier() 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 as HttpError::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.