diff --git a/kiosk/src/server.rs b/kiosk/src/server.rs index 4a938e5..557d555 100644 --- a/kiosk/src/server.rs +++ b/kiosk/src/server.rs @@ -303,14 +303,20 @@ pub fn poll_claim(server: &str, code: &str) -> (String, String) { /// Fetch bundle from server. Returns None on network/HTTP/parse failure. /// On success, also writes the bundle to the on-disk cache. +/// Cached ETag from the last bundle fetch. Sent as If-None-Match so the +/// server can return 304 when the bundle hasn't changed. +static BUNDLE_ETAG: std::sync::Mutex> = std::sync::Mutex::new(None); + pub fn fetch_bundle(server: &str, key: &str) -> Option { let client = reqwest::blocking::Client::new(); - let resp = match client + let mut req = client .get(format!("{server}/api/kiosk/bundle")) .header("Authorization", format!("Bearer {key}")) - .timeout(Duration::from_secs(10)) - .send() - { + .timeout(Duration::from_secs(10)); + if let Some(etag) = BUNDLE_ETAG.lock().unwrap().as_deref() { + req = req.header("If-None-Match", etag); + } + let resp = match req.send() { Ok(r) => r, Err(e) => { tracing::warn!("bundle fetch failed: {e}"); @@ -318,11 +324,21 @@ pub fn fetch_bundle(server: &str, key: &str) -> Option { } }; + // 304 Not Modified — bundle unchanged, use cached. + if resp.status().as_u16() == 304 { + return load_cached_bundle(); + } + if !resp.status().is_success() { tracing::warn!("bundle fetch returned {}", resp.status()); return None; } + // Cache the ETag for next request. + if let Some(etag) = resp.headers().get("etag").and_then(|v| v.to_str().ok()) { + *BUNDLE_ETAG.lock().unwrap() = Some(etag.to_string()); + } + match resp.json::() { Ok(b) => { save_bundle(&b); diff --git a/server/src/plugins/service-api-http/index.ts b/server/src/plugins/service-api-http/index.ts index ea96b20..cebe87c 100644 --- a/server/src/plugins/service-api-http/index.ts +++ b/server/src/plugins/service-api-http/index.ts @@ -297,7 +297,24 @@ function registerKioskRoutes( const bundle = generateBundle(repo, secrets, kiosk.id, clusterKey); if (!bundle) throw createError({ statusCode: 404, statusMessage: "Kiosk not found" }); - return bundle; + // Content-hash ETag: kiosk sends If-None-Match on subsequent fetches. + // If bundle hasn't changed → 304 Not Modified (no body, saves bandwidth). + const json = JSON.stringify(bundle); + const hash = createHash("sha256").update(json).digest("hex").slice(0, 16); + const etag = `"${hash}"`; + const ifNoneMatch = getRequestHeader(event, "if-none-match"); + if (ifNoneMatch === etag) { + return new Response(null, { status: 304 }); + } + + return new Response(json, { + status: 200, + headers: { + "content-type": "application/json", + "etag": etag, + "x-bf-bundle-version": bundle.version, + }, + }); }); // Heartbeat