Make the origin's Cache-Control headers CDN-safe
An audit of the live site ahead of the Cloudflare move found three ways a cache-everything rule would have gone wrong. First, several routes named no policy at all — `/login`, `/issues.json`, `/files/epub/*`, `/files/xtc/*`, `/robots.txt`, the error pages — so the edge would have applied its own default TTL (Cloudflare: two hours on a 200). For `/files/*` that means an authenticated download becoming a publicly cached one. Those routes now say what they mean, and `security_headers` fails closed: a response that set no `Cache-Control` gets `no-store`, so a route added later cannot silently inherit the CDN's default. Everything cookie- or Basic-auth gated (`/files/*`, `/opds*`) is `private, no-store` on every response, 401s and 404s included. Second, the public pages said `max-age=300` alone. They now say `public, max-age=300, s-maxage=86400`: five minutes for the browser, a day for the edge, which is safe because publishing purges the edge. A request carrying a `daily_session=` cookie still gets `private, no-store`. Third, `/static/favicon.svg` and `/static/speculation.json` were referenced without `?v=` while being served `immutable` for a year — editing either one could never have reached a browser again. Both are now in the `ASSET_VERSION` hash and referenced with the version, and `static_asset` only promises a year when the URL actually carries `?v=`; a bare `/static/…` URL gets an hour and revalidates against the same ETag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
This commit is contained in:
1 parent
41fe61a691
commit
7d12f53314
5 files changed
+289
-37
No files matched your search
+23
-4
@@ -339,7 +339,7 @@ pub async fn feed(State(state): State<AppState>) -> Result<Response, WebError> {
|
||||
StatusCode::OK,
|
||||
[
|
||||
(header::CONTENT_TYPE, "application/atom+xml; charset=utf-8"),
|
||||
(header::CACHE_CONTROL, "public, max-age=300"),
|
||||
(header::CACHE_CONTROL, PUBLIC_CACHE),
|
||||
],
|
||||
body,
|
||||
)
|
||||
@@ -349,21 +349,40 @@ pub async fn feed(State(state): State<AppState>) -> Result<Response, WebError> {
|
||||
pub async fn robots() -> Response {
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
|
||||
[
|
||||
(header::CONTENT_TYPE, "text/plain; charset=utf-8"),
|
||||
(header::CACHE_CONTROL, "public, max-age=86400"),
|
||||
],
|
||||
"User-agent: *\nAllow: /\nAllow: /issues\nDisallow: /dashboard\nDisallow: /login\nDisallow: /files\nDisallow: /r\nDisallow: /opds\n",
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
/// `Cache-Control` for an anonymous public page (§3.12).
|
||||
///
|
||||
/// The two ages are aimed at two different caches. `max-age=300` is the
|
||||
/// browser's: a reader who leaves a tab open revalidates within five minutes of
|
||||
/// a new issue landing. `s-maxage=86400` is the CDN's: an issue changes once a
|
||||
/// day, so the edge should be allowed to answer for a day rather than asking
|
||||
/// the origin every five minutes. That long edge life is only safe because
|
||||
/// [`crate::cdn::purge_all`] runs right after a publish; without the purge the
|
||||
/// edge would keep yesterday's paper for its whole day.
|
||||
pub const PUBLIC_CACHE: &str = "public, max-age=300, s-maxage=86400";
|
||||
|
||||
/// `Cache-Control` for anything a signed-in reader sees, and for every
|
||||
/// authenticated download. `private` keeps it out of shared caches even if a
|
||||
/// CDN cache rule is misconfigured; `no-store` keeps it off disk.
|
||||
pub const PRIVATE_CACHE: &str = "private, no-store";
|
||||
|
||||
fn public_cache(mut response: Response, request_headers: &HeaderMap) -> Response {
|
||||
let value = if request_headers
|
||||
.get(header::COOKIE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|cookies| cookies.contains("daily_session="))
|
||||
{
|
||||
"private, no-store"
|
||||
PRIVATE_CACHE
|
||||
} else {
|
||||
"public, max-age=300"
|
||||
PUBLIC_CACHE
|
||||
};
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
|
||||
Reference in new issue
Block a user