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.
This commit is contained in:
Thomas Scholtes 2024-03-01 16:06:40 +01:00 committed by cloudhead
parent 1c4a2cd167
commit e52ce483a1
No known key found for this signature in database
2 changed files with 29 additions and 10 deletions

View File

@ -28,7 +28,7 @@ use radicle_surf::{diff, Glob, Oid, Repository};
use crate::api::error::Error; use crate::api::error::Error;
use crate::api::project::Info; use crate::api::project::Info;
use crate::api::{self, announce_refs, CobsQuery, Context, PaginationQuery, ProjectQuery}; 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 CACHE_1_HOUR: &str = "public, max-age=3600, must-revalidate";
const MAX_BODY_LIMIT: usize = 4_194_304; const MAX_BODY_LIMIT: usize = 4_194_304;
@ -181,7 +181,7 @@ pub struct CommitsQueryString {
} }
/// Get project commit range. /// Get project commit range.
/// `GET /projects/:project/commits?since=<sha>` /// `GET /projects/:project/commits?parent=<sha>`
async fn history_handler( async fn history_handler(
State(ctx): State<Context>, State(ctx): State<Context>,
Path(project): Path<RepoId>, Path(project): Path<RepoId>,
@ -195,6 +195,11 @@ async fn history_handler(
per_page, per_page,
} = qs; } = 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 { let sha = match parent {
Some(commit) => commit, Some(commit) => commit,
None => { None => {
@ -258,7 +263,11 @@ async fn history_handler(
"stats": repo.stats_from(&sha)?, "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. /// 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), "commit": api::json::commit(&commit),
"diff": diff, "diff": diff,
"files": files, "files": files,
"branches": branches "branches": branches
}); });
Ok::<_, Error>(Json(response)) Ok::<_, Error>(immutable_response(response))
} }
/// Get diff between two commits /// Get diff between two commits
@ -391,7 +400,7 @@ async fn diff_handler(
let response = json!({ "diff": diff, "files": files, "commits": commits }); 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. /// Get project activity for the past year.
@ -439,7 +448,7 @@ async fn tree_handler(
if let Some(ref cache) = ctx.cache { if let Some(ref cache) = ctx.cache {
let cache = &mut cache.tree.lock().await; let cache = &mut cache.tree.lock().await;
if let Some(response) = cache.get(&(project, sha, path.clone())) { 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()); cache.put((project, sha, path.clone()), response.clone());
} }
Ok::<_, Error>(Json(response)) Ok::<_, Error>(immutable_response(response))
} }
/// Get all project remotes. /// Get all project remotes.
@ -566,7 +575,7 @@ async fn readme_handler(
{ {
if let Ok(blob) = repo.blob(sha, &path) { if let Ok(blob) = repo.blob(sha, &path) {
let response = api::json::blob(&blob, &path); let response = api::json::blob(&blob, &path);
return Ok::<_, Error>(Json(response)); return Ok::<_, Error>(immutable_response(response));
} }
} }

View File

@ -2,7 +2,8 @@ use axum::extract::path::ErrorKind;
use axum::extract::rejection::{PathRejection, QueryRejection}; use axum::extract::rejection::{PathRejection, QueryRejection};
use axum::extract::FromRequestParts; use axum::extract::FromRequestParts;
use axum::http::request::Parts; use axum::http::request::Parts;
use axum::http::StatusCode; use axum::http::{header, StatusCode};
use axum::response::IntoResponse;
use axum::{async_trait, Json}; use axum::{async_trait, Json};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@ -87,3 +88,12 @@ pub struct Error {
success: bool, success: bool,
error: String, 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),
)
}