Skip to main content

forma_server/
sw.rs

1use axum::{
2    http::header,
3    response::{IntoResponse, Response},
4};
5use rust_embed::Embed;
6
7/// Axum handler: serve the service worker with no-cache headers.
8///
9/// Mount at `/sw.js` in your router. Returns 404 if sw.js is not embedded.
10pub async fn serve_sw<A: Embed>() -> Response {
11    match crate::assets::asset_bytes::<A>("sw.js") {
12        Some(data) => {
13            let mut builder = Response::builder()
14                .header(header::CONTENT_TYPE, "text/javascript; charset=utf-8")
15                .header(header::CACHE_CONTROL, "no-cache, no-store, must-revalidate")
16                .header("service-worker-allowed", "/");
17            // The service worker is the highest-value script on the origin;
18            // it must not be sniffable into another type.
19            for (name, value) in crate::types::asset_security_headers() {
20                builder = builder.header(name, value);
21            }
22            builder
23                .body(axum::body::Body::from(data))
24                .unwrap()
25                .into_response()
26        }
27        None => axum::http::StatusCode::NOT_FOUND.into_response(),
28    }
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34    use rust_embed::Embed;
35
36    #[derive(Embed)]
37    #[folder = "test_fixtures/"]
38    struct Fixtures;
39
40    #[tokio::test]
41    async fn serve_sw_sets_nosniff_and_no_store() {
42        let response = serve_sw::<Fixtures>().await;
43        let headers = response.headers();
44        assert_eq!(
45            headers
46                .get("x-content-type-options")
47                .map(|v| v.to_str().unwrap()),
48            Some("nosniff"),
49            "the service worker must not be sniffable into another type"
50        );
51        assert_eq!(
52            headers
53                .get(header::CONTENT_TYPE)
54                .map(|v| v.to_str().unwrap()),
55            Some("text/javascript; charset=utf-8")
56        );
57        assert!(headers
58            .get(header::CACHE_CONTROL)
59            .unwrap()
60            .to_str()
61            .unwrap()
62            .contains("no-store"));
63    }
64}