Add content-type handling for JSON catalog artifacts and weak ETag for manifests

- Ensured correct `Content-Type` header for catalog artifacts (`catalog.attrs` and `catalog.*`) in HTTP responses.
- Added SHA-1 based weak ETag generation for manifest responses to improve caching and legacy compatibility.
- Updated `integration_tests` to validate content-type and ETag correctness.
- Added new dependency `sha1` for hashing support.
This commit is contained in:
Till Wegmueller 2025-12-09 16:02:02 +01:00
parent e87d1a3166
commit cff3d5d960
No known key found for this signature in database
5 changed files with 38 additions and 3 deletions

1
Cargo.lock generated
View file

@ -2120,6 +2120,7 @@ dependencies = [
"rustls",
"serde",
"serde_json",
"sha1",
"socket2",
"tempfile",
"thiserror 2.0.17",

View file

@ -30,6 +30,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
dirs = "6"
nix = { version = "0.30", features = ["signal", "process", "user", "fs"] }
sha1 = "0.10"
# Telemetry
tracing = "0.1"

View file

@ -7,6 +7,7 @@ use crate::repo::DepotRepo;
use crate::errors::DepotError;
use tower_http::services::ServeFile;
use tower::ServiceExt;
use axum::http::header;
pub async fn get_catalog_v1(
State(repo): State<Arc<DepotRepo>>,
@ -19,7 +20,14 @@ pub async fn get_catalog_v1(
let result = service.oneshot(req).await;
match result {
Ok(res) => Ok(res.into_response()),
Ok(mut res) => {
// Ensure correct content-type for JSON catalog artifacts regardless of file extension
let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog.");
if is_catalog_json {
res.headers_mut().insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"));
}
Ok(res.into_response())
},
Err(e) => Err(DepotError::Server(e.to_string())),
}
}

View file

@ -8,6 +8,7 @@ use crate::repo::DepotRepo;
use crate::errors::DepotError;
use libips::fmri::Fmri;
use std::str::FromStr;
use sha1::Digest as _;
pub async fn get_manifest(
State(repo): State<Arc<DepotRepo>>,
@ -16,9 +17,16 @@ pub async fn get_manifest(
let fmri = Fmri::from_str(&fmri_str).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?;
let content = repo.get_manifest_text(&publisher, &fmri)?;
// Compute weak ETag from SHA-1 of manifest content (legacy friendly)
let mut hasher = sha1::Sha1::new();
hasher.update(content.as_bytes());
let etag = format!("\"{}\"", format!("{:x}", hasher.finalize()));
Ok((
[(header::CONTENT_TYPE, "text/plain")],
content
[
(header::CONTENT_TYPE, "text/plain"),
(header::ETAG, etag.as_str()),
],
content,
).into_response())
}

View file

@ -230,4 +230,21 @@ async fn test_ini_only_repo_serving_catalog() {
let body = resp.text().await.unwrap();
assert!(body.contains("package-count"));
assert!(body.contains("parts"));
// Also fetch individual catalog parts
for part in ["catalog.base.C", "catalog.dependency.C", "catalog.summary.C"].iter() {
let url = format!("{}/{}/catalog/1/{}", base_url, publisher, part);
let resp = client.get(&url).send().await.unwrap();
assert!(resp.status().is_success(), "{} status: {:?}", part, resp.status());
let ct = resp.headers().get("content-type").unwrap().to_str().unwrap().to_string();
assert!(ct.contains("application/json"), "content-type for {} was {}", part, ct);
let txt = resp.text().await.unwrap();
assert!(!txt.is_empty(), "{} should not be empty", part);
if *part == "catalog.base.C" {
assert!(txt.contains(&publisher) && txt.contains("version"), "base part should contain publisher and version");
} else {
// dependency/summary may be empty for this test package; at least ensure signature is present
assert!(txt.contains("_SIGNATURE"), "{} should contain a signature field", part);
}
}
}