mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-03 18:38:21 +00:00
Allow users to override index cache-control
headers (#14620)
## Summary You can now override the cache control headers for the Simple API, file downloads, or both: ```toml [[tool.uv.index]] name = "example" url = "https://example.com/simple" cache-control = { api = "max-age=600", files = "max-age=365000000, immutable" } ``` Closes https://github.com/astral-sh/uv/issues/10444.
This commit is contained in:
parent
9871bbdc79
commit
405ef66cef
8 changed files with 323 additions and 24 deletions
|
@ -196,16 +196,18 @@ impl<E: Into<Self> + std::error::Error + 'static> From<CachedClientError<E>> for
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum CacheControl {
|
||||
pub enum CacheControl<'a> {
|
||||
/// Respect the `cache-control` header from the response.
|
||||
None,
|
||||
/// Apply `max-age=0, must-revalidate` to the request.
|
||||
MustRevalidate,
|
||||
/// Allow the client to return stale responses.
|
||||
AllowStale,
|
||||
/// Override the cache control header with a custom value.
|
||||
Override(&'a str),
|
||||
}
|
||||
|
||||
impl From<Freshness> for CacheControl {
|
||||
impl From<Freshness> for CacheControl<'_> {
|
||||
fn from(value: Freshness) -> Self {
|
||||
match value {
|
||||
Freshness::Fresh => Self::None,
|
||||
|
@ -259,7 +261,7 @@ impl CachedClient {
|
|||
&self,
|
||||
req: Request,
|
||||
cache_entry: &CacheEntry,
|
||||
cache_control: CacheControl,
|
||||
cache_control: CacheControl<'_>,
|
||||
response_callback: Callback,
|
||||
) -> Result<Payload, CachedClientError<CallBackError>> {
|
||||
let payload = self
|
||||
|
@ -292,7 +294,7 @@ impl CachedClient {
|
|||
&self,
|
||||
req: Request,
|
||||
cache_entry: &CacheEntry,
|
||||
cache_control: CacheControl,
|
||||
cache_control: CacheControl<'_>,
|
||||
response_callback: Callback,
|
||||
) -> Result<Payload::Target, CachedClientError<CallBackError>> {
|
||||
let fresh_req = req.try_clone().expect("HTTP request must be cloneable");
|
||||
|
@ -469,7 +471,7 @@ impl CachedClient {
|
|||
async fn send_cached(
|
||||
&self,
|
||||
mut req: Request,
|
||||
cache_control: CacheControl,
|
||||
cache_control: CacheControl<'_>,
|
||||
cached: DataWithCachePolicy,
|
||||
) -> Result<CachedResponse, Error> {
|
||||
// Apply the cache control header, if necessary.
|
||||
|
@ -481,6 +483,13 @@ impl CachedClient {
|
|||
http::HeaderValue::from_static("no-cache"),
|
||||
);
|
||||
}
|
||||
CacheControl::Override(value) => {
|
||||
req.headers_mut().insert(
|
||||
http::header::CACHE_CONTROL,
|
||||
http::HeaderValue::from_str(value)
|
||||
.map_err(|_| ErrorKind::InvalidCacheControl(value.to_string()))?,
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(match cached.cache_policy.before_request(&mut req) {
|
||||
BeforeRequest::Fresh => {
|
||||
|
@ -488,7 +497,7 @@ impl CachedClient {
|
|||
CachedResponse::FreshCache(cached)
|
||||
}
|
||||
BeforeRequest::Stale(new_cache_policy_builder) => match cache_control {
|
||||
CacheControl::None | CacheControl::MustRevalidate => {
|
||||
CacheControl::None | CacheControl::MustRevalidate | CacheControl::Override(_) => {
|
||||
debug!("Found stale response for: {}", req.url());
|
||||
self.send_cached_handle_stale(req, cached, new_cache_policy_builder)
|
||||
.await?
|
||||
|
@ -599,7 +608,7 @@ impl CachedClient {
|
|||
&self,
|
||||
req: Request,
|
||||
cache_entry: &CacheEntry,
|
||||
cache_control: CacheControl,
|
||||
cache_control: CacheControl<'_>,
|
||||
response_callback: Callback,
|
||||
) -> Result<Payload, CachedClientError<CallBackError>> {
|
||||
let payload = self
|
||||
|
@ -623,7 +632,7 @@ impl CachedClient {
|
|||
&self,
|
||||
req: Request,
|
||||
cache_entry: &CacheEntry,
|
||||
cache_control: CacheControl,
|
||||
cache_control: CacheControl<'_>,
|
||||
response_callback: Callback,
|
||||
) -> Result<Payload::Target, CachedClientError<CallBackError>> {
|
||||
let mut past_retries = 0;
|
||||
|
|
|
@ -259,6 +259,9 @@ pub enum ErrorKind {
|
|||
"Network connectivity is disabled, but the requested data wasn't found in the cache for: `{0}`"
|
||||
)]
|
||||
Offline(String),
|
||||
|
||||
#[error("Invalid cache control header: `{0}`")]
|
||||
InvalidCacheControl(String),
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
|
|
|
@ -511,11 +511,17 @@ impl RegistryClient {
|
|||
format!("{package_name}.rkyv"),
|
||||
);
|
||||
let cache_control = match self.connectivity {
|
||||
Connectivity::Online => CacheControl::from(
|
||||
self.cache
|
||||
.freshness(&cache_entry, Some(package_name), None)
|
||||
.map_err(ErrorKind::Io)?,
|
||||
),
|
||||
Connectivity::Online => {
|
||||
if let Some(header) = self.index_urls.simple_api_cache_control_for(index) {
|
||||
CacheControl::Override(header)
|
||||
} else {
|
||||
CacheControl::from(
|
||||
self.cache
|
||||
.freshness(&cache_entry, Some(package_name), None)
|
||||
.map_err(ErrorKind::Io)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
Connectivity::Offline => CacheControl::AllowStale,
|
||||
};
|
||||
|
||||
|
@ -571,7 +577,7 @@ impl RegistryClient {
|
|||
package_name: &PackageName,
|
||||
url: &DisplaySafeUrl,
|
||||
cache_entry: &CacheEntry,
|
||||
cache_control: CacheControl,
|
||||
cache_control: CacheControl<'_>,
|
||||
) -> Result<OwnedArchive<SimpleMetadata>, Error> {
|
||||
let simple_request = self
|
||||
.uncached_client(url)
|
||||
|
@ -783,11 +789,17 @@ impl RegistryClient {
|
|||
format!("{}.msgpack", filename.cache_key()),
|
||||
);
|
||||
let cache_control = match self.connectivity {
|
||||
Connectivity::Online => CacheControl::from(
|
||||
self.cache
|
||||
.freshness(&cache_entry, Some(&filename.name), None)
|
||||
.map_err(ErrorKind::Io)?,
|
||||
),
|
||||
Connectivity::Online => {
|
||||
if let Some(header) = self.index_urls.artifact_cache_control_for(index) {
|
||||
CacheControl::Override(header)
|
||||
} else {
|
||||
CacheControl::from(
|
||||
self.cache
|
||||
.freshness(&cache_entry, Some(&filename.name), None)
|
||||
.map_err(ErrorKind::Io)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
Connectivity::Offline => CacheControl::AllowStale,
|
||||
};
|
||||
|
||||
|
@ -853,11 +865,25 @@ impl RegistryClient {
|
|||
format!("{}.msgpack", filename.cache_key()),
|
||||
);
|
||||
let cache_control = match self.connectivity {
|
||||
Connectivity::Online => CacheControl::from(
|
||||
self.cache
|
||||
.freshness(&cache_entry, Some(&filename.name), None)
|
||||
.map_err(ErrorKind::Io)?,
|
||||
),
|
||||
Connectivity::Online => {
|
||||
if let Some(index) = index {
|
||||
if let Some(header) = self.index_urls.artifact_cache_control_for(index) {
|
||||
CacheControl::Override(header)
|
||||
} else {
|
||||
CacheControl::from(
|
||||
self.cache
|
||||
.freshness(&cache_entry, Some(&filename.name), None)
|
||||
.map_err(ErrorKind::Io)?,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CacheControl::from(
|
||||
self.cache
|
||||
.freshness(&cache_entry, Some(&filename.name), None)
|
||||
.map_err(ErrorKind::Io)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
Connectivity::Offline => CacheControl::AllowStale,
|
||||
};
|
||||
|
||||
|
|
|
@ -6,11 +6,23 @@ use thiserror::Error;
|
|||
|
||||
use uv_auth::{AuthPolicy, Credentials};
|
||||
use uv_redacted::DisplaySafeUrl;
|
||||
use uv_small_str::SmallString;
|
||||
|
||||
use crate::index_name::{IndexName, IndexNameError};
|
||||
use crate::origin::Origin;
|
||||
use crate::{IndexStatusCodeStrategy, IndexUrl, IndexUrlError, SerializableStatusCode};
|
||||
|
||||
/// Cache control configuration for an index.
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IndexCacheControl {
|
||||
/// Cache control header for Simple API requests.
|
||||
pub api: Option<SmallString>,
|
||||
/// Cache control header for file downloads.
|
||||
pub files: Option<SmallString>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
@ -104,6 +116,19 @@ pub struct Index {
|
|||
/// ```
|
||||
#[serde(default)]
|
||||
pub ignore_error_codes: Option<Vec<SerializableStatusCode>>,
|
||||
/// Cache control configuration for this index.
|
||||
///
|
||||
/// When set, these headers will override the server's cache control headers
|
||||
/// for both package metadata requests and artifact downloads.
|
||||
///
|
||||
/// ```toml
|
||||
/// [[tool.uv.index]]
|
||||
/// name = "my-index"
|
||||
/// url = "https://<omitted>/simple"
|
||||
/// cache-control = { api = "max-age=600", files = "max-age=3600" }
|
||||
/// ```
|
||||
#[serde(default)]
|
||||
pub cache_control: Option<IndexCacheControl>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
@ -142,6 +167,7 @@ impl Index {
|
|||
publish_url: None,
|
||||
authenticate: AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,6 +183,7 @@ impl Index {
|
|||
publish_url: None,
|
||||
authenticate: AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,6 +199,7 @@ impl Index {
|
|||
publish_url: None,
|
||||
authenticate: AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,6 +278,7 @@ impl From<IndexUrl> for Index {
|
|||
publish_url: None,
|
||||
authenticate: AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,6 +302,7 @@ impl FromStr for Index {
|
|||
publish_url: None,
|
||||
authenticate: AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -289,6 +319,7 @@ impl FromStr for Index {
|
|||
publish_url: None,
|
||||
authenticate: AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -384,3 +415,55 @@ pub enum IndexSourceError {
|
|||
#[error("Index included a name, but the name was empty")]
|
||||
EmptyName,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_index_cache_control_headers() {
|
||||
// Test that cache control headers are properly parsed from TOML
|
||||
let toml_str = r#"
|
||||
name = "test-index"
|
||||
url = "https://test.example.com/simple"
|
||||
cache-control = { api = "max-age=600", files = "max-age=3600" }
|
||||
"#;
|
||||
|
||||
let index: Index = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
|
||||
assert!(index.cache_control.is_some());
|
||||
let cache_control = index.cache_control.as_ref().unwrap();
|
||||
assert_eq!(cache_control.api.as_deref(), Some("max-age=600"));
|
||||
assert_eq!(cache_control.files.as_deref(), Some("max-age=3600"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_without_cache_control() {
|
||||
// Test that indexes work without cache control headers
|
||||
let toml_str = r#"
|
||||
name = "test-index"
|
||||
url = "https://test.example.com/simple"
|
||||
"#;
|
||||
|
||||
let index: Index = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
|
||||
assert_eq!(index.cache_control, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index_partial_cache_control() {
|
||||
// Test that cache control can have just one field
|
||||
let toml_str = r#"
|
||||
name = "test-index"
|
||||
url = "https://test.example.com/simple"
|
||||
cache-control = { api = "max-age=300" }
|
||||
"#;
|
||||
|
||||
let index: Index = toml::from_str(toml_str).unwrap();
|
||||
assert_eq!(index.name.as_ref().unwrap().as_ref(), "test-index");
|
||||
assert!(index.cache_control.is_some());
|
||||
let cache_control = index.cache_control.as_ref().unwrap();
|
||||
assert_eq!(cache_control.api.as_deref(), Some("max-age=300"));
|
||||
assert_eq!(cache_control.files, None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -599,6 +599,26 @@ impl<'a> IndexUrls {
|
|||
}
|
||||
IndexStatusCodeStrategy::Default
|
||||
}
|
||||
|
||||
/// Return the Simple API cache control header for an [`IndexUrl`], if configured.
|
||||
pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<&str> {
|
||||
for index in &self.indexes {
|
||||
if index.url() == url {
|
||||
return index.cache_control.as_ref()?.api.as_deref();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the artifact cache control header for an [`IndexUrl`], if configured.
|
||||
pub fn artifact_cache_control_for(&self, url: &IndexUrl) -> Option<&str> {
|
||||
for index in &self.indexes {
|
||||
if index.url() == url {
|
||||
return index.cache_control.as_ref()?.files.as_deref();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
|
@ -717,4 +737,64 @@ mod tests {
|
|||
"git+https://github.com/example/repo.git"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_control_lookup() {
|
||||
use std::str::FromStr;
|
||||
|
||||
use uv_small_str::SmallString;
|
||||
|
||||
use crate::IndexFormat;
|
||||
use crate::index_name::IndexName;
|
||||
|
||||
let indexes = vec![
|
||||
Index {
|
||||
name: Some(IndexName::from_str("index1").unwrap()),
|
||||
url: IndexUrl::from_str("https://index1.example.com/simple").unwrap(),
|
||||
cache_control: Some(crate::IndexCacheControl {
|
||||
api: Some(SmallString::from("max-age=300")),
|
||||
files: Some(SmallString::from("max-age=1800")),
|
||||
}),
|
||||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
format: IndexFormat::Simple,
|
||||
publish_url: None,
|
||||
authenticate: uv_auth::AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
},
|
||||
Index {
|
||||
name: Some(IndexName::from_str("index2").unwrap()),
|
||||
url: IndexUrl::from_str("https://index2.example.com/simple").unwrap(),
|
||||
cache_control: None,
|
||||
explicit: false,
|
||||
default: false,
|
||||
origin: None,
|
||||
format: IndexFormat::Simple,
|
||||
publish_url: None,
|
||||
authenticate: uv_auth::AuthPolicy::default(),
|
||||
ignore_error_codes: None,
|
||||
},
|
||||
];
|
||||
|
||||
let index_urls = IndexUrls::from_indexes(indexes);
|
||||
|
||||
let url1 = IndexUrl::from_str("https://index1.example.com/simple").unwrap();
|
||||
assert_eq!(
|
||||
index_urls.simple_api_cache_control_for(&url1),
|
||||
Some("max-age=300")
|
||||
);
|
||||
assert_eq!(
|
||||
index_urls.artifact_cache_control_for(&url1),
|
||||
Some("max-age=1800")
|
||||
);
|
||||
|
||||
let url2 = IndexUrl::from_str("https://index2.example.com/simple").unwrap();
|
||||
assert_eq!(index_urls.simple_api_cache_control_for(&url2), None);
|
||||
assert_eq!(index_urls.artifact_cache_control_for(&url2), None);
|
||||
|
||||
let url3 = IndexUrl::from_str("https://index3.example.com/simple").unwrap();
|
||||
assert_eq!(index_urls.simple_api_cache_control_for(&url3), None);
|
||||
assert_eq!(index_urls.artifact_cache_control_for(&url3), None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,6 +139,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -320,6 +321,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -502,6 +504,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -716,6 +719,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -1059,6 +1063,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -1267,6 +1272,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -1299,6 +1305,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -1484,6 +1491,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -1516,6 +1524,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -1548,6 +1557,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -1755,6 +1765,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
no_index: true,
|
||||
|
@ -2124,6 +2135,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -2156,6 +2168,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -2337,6 +2350,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -2369,6 +2383,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -3564,6 +3579,7 @@ fn resolve_both() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -3870,6 +3886,7 @@ fn resolve_config_file() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -4658,6 +4675,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -4690,6 +4708,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -4873,6 +4892,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -4905,6 +4925,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -5094,6 +5115,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -5126,6 +5148,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -5310,6 +5333,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -5342,6 +5366,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -5533,6 +5558,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -5565,6 +5591,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
@ -5749,6 +5776,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
Index {
|
||||
name: None,
|
||||
|
@ -5781,6 +5809,7 @@ fn index_priority() -> anyhow::Result<()> {
|
|||
publish_url: None,
|
||||
authenticate: Auto,
|
||||
ignore_error_codes: None,
|
||||
cache_control: None,
|
||||
},
|
||||
],
|
||||
flat_index: [],
|
||||
|
|
|
@ -244,6 +244,43 @@ authenticate = "never"
|
|||
When `authenticate` is set to `never`, uv will never search for credentials for the given index and
|
||||
will error if credentials are provided directly.
|
||||
|
||||
### Customizing cache control headers
|
||||
|
||||
By default, uv will respect the cache control headers provided by the index. For example, PyPI
|
||||
serves package metadata with a `max-age=600` header, thereby allowing uv to cache package metadata
|
||||
for 10 minutes; and wheels and source distributions with a `max-age=365000000, immutable` header,
|
||||
thereby allowing uv to cache artifacts indefinitely.
|
||||
|
||||
To override the cache control headers for an index, use the `cache-control` setting:
|
||||
|
||||
```toml
|
||||
[[tool.uv.index]]
|
||||
name = "example"
|
||||
url = "https://example.com/simple"
|
||||
cache-control = { api = "max-age=600", files = "max-age=365000000, immutable" }
|
||||
```
|
||||
|
||||
The `cache-control` setting accepts an object with two optional keys:
|
||||
|
||||
- `api`: Controls caching for Simple API requests (package metadata).
|
||||
- `files`: Controls caching for artifact downloads (wheels and source distributions).
|
||||
|
||||
The values for these keys are strings that follow the
|
||||
[HTTP Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control)
|
||||
syntax. For example, to force uv to always revalidate package metadata, set `api = "no-cache"`:
|
||||
|
||||
```toml
|
||||
[[tool.uv.index]]
|
||||
name = "example"
|
||||
url = "https://example.com/simple"
|
||||
cache-control = { api = "no-cache" }
|
||||
```
|
||||
|
||||
This setting is most commonly used to override the default cache control headers for private indexes
|
||||
that otherwise disable caching, often unintentionally. We typically recommend following PyPI's
|
||||
approach to caching headers, i.e., setting `api = "max-age=600"` and
|
||||
`files = "max-age=365000000, immutable"`.
|
||||
|
||||
## "Flat" indexes
|
||||
|
||||
By default, `[[tool.uv.index]]` entries are assumed to be PyPI-style registries that implement the
|
||||
|
|
32
uv.schema.json
generated
32
uv.schema.json
generated
|
@ -907,6 +907,18 @@
|
|||
],
|
||||
"default": "auto"
|
||||
},
|
||||
"cache-control": {
|
||||
"description": "Cache control configuration for this index.\n\nWhen set, these headers will override the server's cache control headers\nfor both package metadata requests and artifact downloads.\n\n```toml\n[[tool.uv.index]]\nname = \"my-index\"\nurl = \"https://<omitted>/simple\"\ncache-control = { api = \"max-age=600\", files = \"max-age=3600\" }\n```",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/IndexCacheControl"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
},
|
||||
"default": {
|
||||
"description": "Mark the index as the default index.\n\nBy default, uv uses PyPI as the default index, such that even if additional indexes are\ndefined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that\naren't found elsewhere. To disable the PyPI default, set `default = true` on at least one\nother index.\n\nMarking an index as default will move it to the front of the list of indexes, such that it\nis given the highest priority when resolving packages.",
|
||||
"type": "boolean",
|
||||
|
@ -972,6 +984,26 @@
|
|||
"url"
|
||||
]
|
||||
},
|
||||
"IndexCacheControl": {
|
||||
"description": "Cache control configuration for an index.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api": {
|
||||
"description": "Cache control header for Simple API requests.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"files": {
|
||||
"description": "Cache control header for file downloads.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"IndexFormat": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue