diff --git a/crates/http-proto/src/response.rs b/crates/http-proto/src/response.rs index 112aa8f..6a19bf8 100644 --- a/crates/http-proto/src/response.rs +++ b/crates/http-proto/src/response.rs @@ -180,6 +180,22 @@ impl HttpResponse { self } + /// inbuxa: the same, for a response that required authentication. + /// + /// `public` lets any cache keep the body and hand it to anyone, and + /// `immutable` means a browser will not revalidate it for a year. On a + /// response whose headers depend on the request's `Origin`, that is a trap: + /// an entry stored while the origin was not yet an allowed front end has no + /// CORS headers and no `Vary`, and is then replayed from cache to a caller + /// that would have been allowed, which fails as an opaque network error + /// with nothing on the server to show for it. + pub fn with_private_immutable_cache(mut self) -> Self { + self.builder = self + .builder + .header(header::CACHE_CONTROL, "private, max-age=31536000, immutable"); + self + } + pub fn with_location(mut self, location: V) -> Self where V: TryInto, diff --git a/crates/http/src/api/mod.rs b/crates/http/src/api/mod.rs index 27faa80..436b902 100644 --- a/crates/http/src/api/mod.rs +++ b/crates/http/src/api/mod.rs @@ -117,9 +117,13 @@ impl ManagementApi for Server { include_str!("../../../../resources/schema/schema.json.sha256"); if path.get(1).is_some_and(|hash| hash == &SCHEMA_HASH) { + // inbuxa: private, not public. This is behind + // authenticate_headers and its CORS headers vary by + // Origin, so a shared or origin-agnostic cache entry is + // wrong -- and, being immutable, wrong for a year. Ok(Resource::new("application/json", SCHEMA_JSON.to_vec()) .into_http_response() - .with_immutable_cache() + .with_private_immutable_cache() .with_header(CONTENT_ENCODING, "gzip")) } else { Ok(HttpResponse::redirect(format!("/api/schema/{SCHEMA_HASH}"))) diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index d7d8ce4..5366e6c 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -812,13 +812,20 @@ async fn handle_session(inner: Arc, session: SessionDat // (contract C-14). Responses that already set their own // CORS headers, such as public discovery metadata, keep // them (C-15). + // inbuxa: Vary goes on every response, not only when an + // origin list exists. A response cached while the list was + // empty -- before the front ends were configured -- would + // otherwise carry neither CORS headers nor Vary, and a + // cache would replay it to an origin that should have been + // allowed. With `immutable` on some of these, that is a + // year of an opaque failure the server never sees. let cors_origins = &server.core.network.http.cors_origins; + response.headers_mut().append( + hyper::header::VARY, + hyper::header::HeaderValue::from_static("Origin"), + ); if !cors_origins.is_empty() { let headers = response.headers_mut(); - headers.append( - hyper::header::VARY, - hyper::header::HeaderValue::from_static("Origin"), - ); if let Some(origin) = origin.filter(|origin| { cors_origins.contains(origin) && !headers.contains_key(hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN)