From c55705b380144434b987c708c1f739ae44a1ae0a Mon Sep 17 00:00:00 2001 From: Jacob Hoffman-Andrews Date: Tue, 30 Nov 2021 21:22:40 -0800 Subject: [PATCH] Add Cache-Control to rustdoc pages For /latest/ URLs, set max-age=0. For versioned URLs max-age=10 minutes and stale-while-revalidate=2 months. The idea behind this is that versioned URLs change mostly in minor ways - the "Go to latest" link at the top, and the list of versions in the crate menu. And setting a long cache time (either via max-age or via stale-while-revalidate) allows pages to be loaded even while offline. We could probably apply a long stale-while-revalidate to /latest/ URLs as well, but this is more likely to have a user-noticeable impact, and the /latest/ URLs are relatively new so we don't want to create any confusing interactions. --- src/web/rustdoc.rs | 47 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index e2c37d5a6..04ab55fe2 100644 --- a/src/web/rustdoc.rs +++ b/src/web/rustdoc.rs @@ -11,7 +11,10 @@ use crate::{ Config, Metrics, Storage, }; use anyhow::{anyhow, Context}; -use iron::url::percent_encoding::percent_decode; +use iron::{ + headers::{CacheControl, CacheDirective}, + url::percent_encoding::percent_decode, +}; use iron::{ headers::{Expires, HttpDate}, modifiers::Redirect, @@ -198,7 +201,11 @@ struct RustdocPage { latest_version: String, target: String, inner_path: String, + // true if we are displaying the latest version of the crate, regardless + // of whether the URL specifies a version number or the string "latest." is_latest_version: bool, + // true if the URL specifies a version using the string "latest." + is_latest_url: bool, is_prerelease: bool, krate: CrateDetails, metadata: MetaData, @@ -224,6 +231,7 @@ impl RustdocPage { .get::() .expect("missing Metrics from the request extensions"); + let is_latest_url = self.is_latest_url; // Build the page of documentation let ctx = ctry!(req, tera::Context::from_serialize(self)); // Extract the head and body of the rustdoc file so that we can insert it into our own html @@ -245,7 +253,19 @@ impl RustdocPage { let mut response = Response::with((Status::Ok, html)); response.headers.set(ContentType::html()); - + if is_latest_url { + response + .headers + .set(CacheControl(vec![CacheDirective::MaxAge(0)])); + } else { + response.headers.set(CacheControl(vec![ + CacheDirective::Extension( + "stale-while-revalidate".to_string(), + Some("2592000".to_string()), // sixty days + ), + CacheDirective::MaxAge(600u32), // ten minutes + ])); + } Ok(response) } } @@ -492,6 +512,7 @@ pub fn rustdoc_html_server_handler(req: &mut Request) -> IronResult { target, inner_path, is_latest_version, + is_latest_url: version_or_latest == "latest", is_prerelease, metadata: krate.metadata.clone(), krate, @@ -831,6 +852,28 @@ mod test { }) } + #[test] + fn cache_headers() { + wrapper(|env| { + env.fake_release() + .name("dummy") + .version("0.1.0") + .archive_storage(true) + .rustdoc_file("dummy/index.html") + .create()?; + + let resp = env.frontend().get("/dummy/latest/dummy/").send()?; + assert_eq!(resp.headers().get("Cache-Control").unwrap(), &"max-age=0"); + + let resp = env.frontend().get("/dummy/0.1.0/dummy/").send()?; + assert_eq!( + resp.headers().get("Cache-Control").unwrap(), + &"stale-while-revalidate=2592000, max-age=600" + ); + Ok(()) + }) + } + #[test_case(true)] #[test_case(false)] fn go_to_latest_version(archive_storage: bool) {