Embed external Static File with cache buster
The problem in both of the previous cases is that the browser will try to cache the content of the CSS file. That means that even if we change the content of the CSS file inside the Rust file or even in the external file, the browser will not download the new version and we won’t see the change.
We could manually clear the cache of the browser, but the end users have the same problem. If we deploy a new version of the CSS file the clients will not see the content until the cache expires.
This is a well known problem in the web development world.
We need a cache buster. One of the solutions is to change the name of the static file every time it changes.However one needs to be careful to also change the reference to the file to match the new filename.
- const-hex
- xxhash-rust used for hashing the content of the CSS file at compile time.
- This idea was taken from rgit and gitore.
[package]
name = "embed-external-static-file"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
axum = "0.8.8"
const-hex = "1.18.1"
const_format = "0.2.35"
tokio = { version = "1.50.0", features = ["full"] }
xxhash-rust = { version = "0.8.15", features = ["const_xxh3", "xxh3"] }
[dev-dependencies]
headers = "0.4.1"
http-body-util = "0.1.3"
tower = { version = "0.5.3", features = ["util"] }
Code
use axum::{
Router,
http::header::{self, HeaderMap},
response::{Html, IntoResponse},
routing::get,
};
use const_format::formatcp;
use xxhash_rust::const_xxh3;
const STYLE_CSS: &[u8] = include_bytes!("static/style.css");
const STYLE_CSS_HASH: &str = const_hex::Buffer::<16, false>::new()
.const_format(&const_xxh3::xxh3_128(STYLE_CSS).to_be_bytes())
.as_str();
async fn handle_main_page() -> Html<String> {
Html(format!(
r#"
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/css/{STYLE_CSS_HASH}-style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Embedded static file</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
"#
))
}
async fn send_style_css() -> impl IntoResponse {
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "text/css".parse().unwrap());
(headers, STYLE_CSS)
}
fn create_router() -> Router {
Router::new().route("/", get(handle_main_page)).route(
formatcp!("/static/css/{}-style.css", STYLE_CSS_HASH),
get(send_style_css),
)
}
#[tokio::main]
async fn main() {
let app = create_router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("listening on http://{}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
#[cfg(test)]
mod tests;
CSS file
h1 {
color: green;
}
Tests
#![allow(unused)]
fn main() {
use axum::{body::Body, http::Request, http::StatusCode};
use http_body_util::BodyExt;
use tower::ServiceExt;
use super::*;
#[tokio::test]
async fn test_main_page() {
let response = create_router()
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let content_type = response.headers().get("content-type").unwrap();
assert_eq!(content_type.to_str().unwrap(), "text/html; charset=utf-8");
let body = response.into_body();
let bytes = body.collect().await.unwrap().to_bytes();
let html = String::from_utf8(bytes.to_vec()).unwrap();
assert!(html.contains("<h1>Hello, World!</h1>"));
// <link rel="stylesheet" href="/static/css/0d5d21cbd55d2e87fc6dd2713c288a24-style.css">
}
}