web.rs
⎇
Raw
1//! Serving the built web UI out of the binary.
2//!
3//! `rust-embed` compiles `web/dist` in for release builds, so a deployment is
4//! still one file. In debug builds it reads from disk instead, so `vite build`
5//! output is picked up without a `cargo` rebuild.
6//!
7//! Anything that is not an embedded asset falls back to `index.html`, because the
8//! UI routes client-side and a deep link must not 404.
9
10use axum::http::{StatusCode, Uri, header};
11use axum::response::{IntoResponse, Response};
12
13#[derive(rust_embed::Embed)]
14#[folder = "../../web/dist"]
15struct Assets;
16
17/// The message shown when the binary was built without a web UI. Silence here
18/// would look like a broken deployment rather than a missing build step.
19const NO_BUILD: &str = "the web UI is not built. Run `cd web && bun install && bun run build`.";
20
21pub async fn serve(uri: Uri) -> Response {
22 let path = uri.path().trim_start_matches('/');
23 if let Some(response) = asset(path) {
24 return response;
25 }
26 // Never fall back for asset-shaped requests: a missing chunk answered with
27 // HTML turns a clear 404 into a confusing MIME-type error in the console.
28 if path.starts_with("assets/") {
29 return (StatusCode::NOT_FOUND, "not found").into_response();
30 }
31 asset("index.html").unwrap_or_else(|| (StatusCode::NOT_FOUND, NO_BUILD).into_response())
32}
33
34fn asset(path: &str) -> Option<Response> {
35 let file = Assets::get(path)?;
36 let mime = mime_guess::from_path(path).first_or_octet_stream();
37 // Vite content-hashes everything under assets/, so those are immutable.
38 // index.html must not be, or a deploy would never reach an open tab.
39 let cache = if path.starts_with("assets/") {
40 "public, max-age=31536000, immutable"
41 } else {
42 "no-cache"
43 };
44 Some(
45 (
46 [
47 (header::CONTENT_TYPE, mime.as_ref()),
48 (header::CACHE_CONTROL, cache),
49 ],
50 file.data.into_owned(),
51 )
52 .into_response(),
53 )
54}
55