Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

[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">
}
}