Skip to content

Commit e53afba

Browse files
authored
🐛 fix(config): reject negative cache timing (#1785)
1 parent 48a8731 commit e53afba

5 files changed

Lines changed: 60 additions & 4 deletions

File tree

crates/peryx/src/config/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,6 @@ pub enum ConfigError {
9292
Tls { reason: &'static str },
9393
#[error("invalid environment variable {var}: {reason}")]
9494
Env { var: &'static str, reason: String },
95+
#[error("`{field}` must be non-negative, got {value}")]
96+
CacheTiming { field: &'static str, value: i64 },
9597
}

crates/peryx/src/config/model.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,17 @@ impl Config {
365365
}
366366

367367
/// # Errors
368-
/// Returns an error when plugin authentication, identity providers, jobs, durability, or writer
369-
/// identity conflict with the resolved configuration.
368+
/// Returns an error when cache timing, plugin authentication, identity providers, jobs,
369+
/// durability, or writer identity conflict with the resolved configuration.
370370
pub fn validate_with_plugins(&self, plugins: &peryx_plugin_registry::PluginRegistry) -> Result<(), ConfigError> {
371+
for (field, value) in [
372+
("cache_ttl_secs", self.cache_ttl_secs),
373+
("max_stale_secs", self.max_stale_secs),
374+
] {
375+
if value < 0 {
376+
return Err(ConfigError::CacheTiming { field, value });
377+
}
378+
}
371379
let plugin_indexes = self
372380
.indexes
373381
.iter()

crates/peryx/tests/cli_entrypoint.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,21 @@ fn runtime_command_rejects_an_invalid_host(#[case] command: &[&str]) {
370370
assert_failure(&output, "`host` \"not a host\"");
371371
}
372372

373+
#[rstest]
374+
#[case::serve_freshness(&["serve"], "PERYX_CACHE_TTL_SECS", "cache_ttl_secs")]
375+
#[case::serve_stale_bound(&["serve"], "PERYX_MAX_STALE_SECS", "max_stale_secs")]
376+
#[case::check_freshness(&["config", "check"], "PERYX_CACHE_TTL_SECS", "cache_ttl_secs")]
377+
#[case::check_stale_bound(&["config", "check"], "PERYX_MAX_STALE_SECS", "max_stale_secs")]
378+
fn runtime_commands_reject_negative_cache_timing(
379+
#[case] command: &[&str],
380+
#[case] variable: &str,
381+
#[case] field: &str,
382+
) {
383+
let output = peryx().args(command).env(variable, "-1").output().unwrap();
384+
385+
assert_failure(&output, &format!("`{field}` must be non-negative, got -1"));
386+
}
387+
373388
#[test]
374389
fn runtime_arguments_override_environment_and_selected_config() {
375390
let dir = tempfile::tempdir().unwrap();

crates/peryx/tests/unit/tests/config/model_tests.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,36 @@ fn test_default_config() {
8888
assert_eq!(c.rate_limit, RateLimitConfig::default());
8989
}
9090

91+
#[rstest::rstest]
92+
#[case::freshness(-1, 0, "`cache_ttl_secs` must be non-negative, got -1")]
93+
#[case::stale_bound(0, -2, "`max_stale_secs` must be non-negative, got -2")]
94+
fn test_config_rejects_negative_cache_timing(
95+
#[case] cache_ttl_secs: i64,
96+
#[case] max_stale_secs: i64,
97+
#[case] expected: &str,
98+
) {
99+
let config = Config {
100+
cache_ttl_secs,
101+
max_stale_secs,
102+
..Config::default()
103+
};
104+
105+
assert_eq!(config.validate().unwrap_err().to_string(), expected);
106+
}
107+
108+
#[rstest::rstest]
109+
#[case::zero(0)]
110+
#[case::positive(1)]
111+
fn test_config_accepts_non_negative_cache_timing(#[case] value: i64) {
112+
Config {
113+
cache_ttl_secs: value,
114+
max_stale_secs: value,
115+
..Config::default()
116+
}
117+
.validate()
118+
.unwrap();
119+
}
120+
91121
#[rstest::rstest]
92122
#[case::ipv4("127.0.0.1", "127.0.0.1:4433")]
93123
#[case::ipv6_wildcard("::", "[::]:4433")]

site/content/core/operations/configuration.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ selected socket address. `serve` passes its selected address to the listener wit
4747

4848
The ceiling limits how long peryx trusts an upstream `Cache-Control` value. An upstream or CDN that answers
4949
`max-age=31536000` would otherwise pin a page in the cache for a year with no revalidation. Raise `cache_ttl_secs` if
50-
you want to trust a long upstream lifetime; lower it to revalidate sooner than the upstream asks.
50+
you want to trust a long upstream lifetime; lower it to revalidate sooner than the upstream asks. Set it to `0` to
51+
revalidate every request. `cache_ttl_secs` must be non-negative.
5152

5253
Artifacts never expire; they are content-addressed by sha256, so a changed upstream file is a new entry on the page
5354
rather than a mutation.
@@ -56,7 +57,7 @@ rather than a mutation.
5657
last page it fetched, but only for this long past the page's freshness window. Beyond it the upstream failure surfaces
5758
instead, because a cache that answers with whatever it last saw, forever, has stopped being a cache and become a fork.
5859
Set it to `0` to serve stale without limit, which is what mirroring a knowingly unreliable upstream asks for;
59-
`offline = true` below is the unconditional form.
60+
`offline = true` below is the unconditional form. `max_stale_secs` must be non-negative.
6061

6162
`usage_retention_days` bounds the durable
6263
[daily group and source usage](@/core/operations/monitor.md#daily-group-and-source-usage) aggregate: buckets older than

0 commit comments

Comments
 (0)