diff --git a/src/config.rs b/src/config.rs index ecdfaf40e..631d6672b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -57,6 +57,12 @@ pub struct Config { // Content Security Policy pub(crate) csp_report_only: bool, + // Cache-Control header + // If both are absent, don't generate the header. If only one is present, + // generate just that directive. Values are in seconds. + pub(crate) cache_control_stale_while_revalidate: Option, + pub(crate) cache_control_max_age: Option, + // Build params pub(crate) build_attempts: u16, pub(crate) rustwide_workspace: PathBuf, @@ -130,6 +136,11 @@ impl Config { csp_report_only: env("DOCSRS_CSP_REPORT_ONLY", false)?, + cache_control_stale_while_revalidate: maybe_env( + "CACHE_CONTROL_STALE_WHILE_REVALIDATE", + )?, + cache_control_max_age: maybe_env("CACHE_CONTROL_MAX_AGE")?, + local_archive_cache_path: env( "DOCSRS_ARCHIVE_INDEX_CACHE_PATH", prefix.join("archive_cache"), diff --git a/src/web/rustdoc.rs b/src/web/rustdoc.rs index bf5e23e2a..12d824984 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, @@ -199,7 +202,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, @@ -225,15 +232,16 @@ 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)); + let config = extension!(req, Config); // Extract the head and body of the rustdoc file so that we can insert it into our own html // while logging OOM errors from html rewriting let html = match utils::rewrite_lol(rustdoc_html, max_parse_memory, ctx, templates) { Err(RewritingError::MemoryLimitExceeded(..)) => { metrics.html_rewrite_ooms.inc(); - let config = extension!(req, Config); let err = anyhow!( "Failed to serve the rustdoc file '{}' because rewriting it surpassed the memory limit of {} bytes", file_path, config.max_parse_memory, @@ -246,7 +254,27 @@ 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 { + let mut directives = vec![]; + if let Some(seconds) = config.cache_control_stale_while_revalidate { + directives.push(CacheDirective::Extension( + "stale-while-revalidate".to_string(), + Some(format!("{}", seconds)), + )); + } + + if let Some(seconds) = config.cache_control_max_age { + directives.push(CacheDirective::MaxAge(seconds)); + } + if !directives.is_empty() { + response.headers.set(CacheControl(directives)); + } + } Ok(response) } } @@ -501,6 +529,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, @@ -840,6 +869,32 @@ mod test { }) } + #[test] + fn cache_headers() { + wrapper(|env| { + env.override_config(|config| { + config.cache_control_max_age = Some(600); + config.cache_control_stale_while_revalidate = Some(2592000); + }); + + 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) {