From e52ce483a18d255e911f3f19a8fb9011df8ae91d Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Fri, 1 Mar 2024 16:06:40 +0100 Subject: [PATCH] httpd: Mark immutable response as such with Cache-Control header For responses to requests for content that is content-addressed with Git object IDs and consequently immutable, we add a Cache-Control HTTP header that marks the responses as public, immutable and instructs clients to cache the response for up to 7 days. --- radicle-httpd/src/api/v1/projects.rs | 27 ++++++++++++++++++--------- radicle-httpd/src/axum_extra.rs | 12 +++++++++++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/radicle-httpd/src/api/v1/projects.rs b/radicle-httpd/src/api/v1/projects.rs index 8739907b..cfc9abd0 100644 --- a/radicle-httpd/src/api/v1/projects.rs +++ b/radicle-httpd/src/api/v1/projects.rs @@ -28,7 +28,7 @@ use radicle_surf::{diff, Glob, Oid, Repository}; use crate::api::error::Error; use crate::api::project::Info; use crate::api::{self, announce_refs, CobsQuery, Context, PaginationQuery, ProjectQuery}; -use crate::axum_extra::{Path, Query}; +use crate::axum_extra::{immutable_response, Path, Query}; const CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate"; const MAX_BODY_LIMIT: usize = 4_194_304; @@ -181,7 +181,7 @@ pub struct CommitsQueryString { } /// Get project commit range. -/// `GET /projects/:project/commits?since=` +/// `GET /projects/:project/commits?parent=` async fn history_handler( State(ctx): State, Path(project): Path, @@ -195,6 +195,11 @@ async fn history_handler( per_page, } = qs; + // If the parent commit is provided, the response depends only on the query + // string and not on the state of the repository. This means we can instruct + // the caches to treat the response as immutable. + let is_immutable = parent.is_some(); + let sha = match parent { Some(commit) => commit, None => { @@ -258,7 +263,11 @@ async fn history_handler( "stats": repo.stats_from(&sha)?, }); - Ok::<_, Error>((StatusCode::OK, Json(response))) + if is_immutable { + Ok::<_, Error>(immutable_response(response).into_response()) + } else { + Ok::<_, Error>(Json(response).into_response()) + } } /// Get project commit. @@ -318,13 +327,13 @@ async fn commit_handler( } }); - let response = json!({ + let response: serde_json::Value = json!({ "commit": api::json::commit(&commit), "diff": diff, "files": files, "branches": branches }); - Ok::<_, Error>(Json(response)) + Ok::<_, Error>(immutable_response(response)) } /// Get diff between two commits @@ -391,7 +400,7 @@ async fn diff_handler( let response = json!({ "diff": diff, "files": files, "commits": commits }); - Ok::<_, Error>(Json(response)) + Ok::<_, Error>(immutable_response(response)) } /// Get project activity for the past year. @@ -439,7 +448,7 @@ async fn tree_handler( if let Some(ref cache) = ctx.cache { let cache = &mut cache.tree.lock().await; if let Some(response) = cache.get(&(project, sha, path.clone())) { - return Ok::<_, Error>(Json(response.clone())); + return Ok::<_, Error>(immutable_response(response.clone())); } } @@ -454,7 +463,7 @@ async fn tree_handler( cache.put((project, sha, path.clone()), response.clone()); } - Ok::<_, Error>(Json(response)) + Ok::<_, Error>(immutable_response(response)) } /// Get all project remotes. @@ -566,7 +575,7 @@ async fn readme_handler( { if let Ok(blob) = repo.blob(sha, &path) { let response = api::json::blob(&blob, &path); - return Ok::<_, Error>(Json(response)); + return Ok::<_, Error>(immutable_response(response)); } } diff --git a/radicle-httpd/src/axum_extra.rs b/radicle-httpd/src/axum_extra.rs index 79a23e13..3bdfcb6a 100644 --- a/radicle-httpd/src/axum_extra.rs +++ b/radicle-httpd/src/axum_extra.rs @@ -2,7 +2,8 @@ use axum::extract::path::ErrorKind; use axum::extract::rejection::{PathRejection, QueryRejection}; use axum::extract::FromRequestParts; use axum::http::request::Parts; -use axum::http::StatusCode; +use axum::http::{header, StatusCode}; +use axum::response::IntoResponse; use axum::{async_trait, Json}; use serde::de::DeserializeOwned; @@ -87,3 +88,12 @@ pub struct Error { success: bool, error: String, } + +/// Add a Cache-Control header that marks the response as immutable and +/// instructs clients to cache the response for 7 days. +pub fn immutable_response(data: impl serde::Serialize) -> impl IntoResponse { + ( + [(header::CACHE_CONTROL, "public, max-age=604800, immutable")], + Json(data), + ) +}