Context
ProxyState.config_apply_age (crates/aisix-proxy/src/state.rs) is the probe behind GET /readyz's config-freshness gate. Every constructor defaults it to None, and the handler treats a None field as "config ok unconditionally" (readiness gates on shutdown only):
let config_block = state
.config_apply_age
.as_ref()
.and_then(|probe| crate::health::config_readiness_block(probe()));
Production relies on aisix-server wiring with_config_apply_age in every mode (etcd/managed → supervisor.watch_status(); file → always-fresh). If a refactor ever drops that wiring — e.g. while reworking the adjacent admin-listener bootstrap during the Admin API removal — /readyz silently downgrades to shutdown-only readiness and every test stays green, because the field-None output is byte-identical to wired-and-fresh ([+]config ok). That would re-open the class of #591: an orchestrator routing traffic to a not-yet-configured or stale-watch instance.
A router-level unit test now pins the plumbing (a wired probe returning None → 503), but it cannot pin the wiring itself.
Proposed fix
Make the production constructor fail-closed: default the probe to || None ("no freshness signal = not ready") in ProxyState::new/with_components, and have tests that don't care about readiness opt out explicitly (e.g. .with_config_always_fresh()). Then a dropped wiring makes /readyz 503 permanently — caught loudly by the harness readiness gate and every e2e — instead of passing silently.
Surfaced by the independent audit of #802.
Context
ProxyState.config_apply_age(crates/aisix-proxy/src/state.rs) is the probe behindGET /readyz's config-freshness gate. Every constructor defaults it toNone, and the handler treats aNonefield as "config ok unconditionally" (readiness gates on shutdown only):Production relies on
aisix-serverwiringwith_config_apply_agein every mode (etcd/managed →supervisor.watch_status(); file → always-fresh). If a refactor ever drops that wiring — e.g. while reworking the adjacent admin-listener bootstrap during the Admin API removal —/readyzsilently downgrades to shutdown-only readiness and every test stays green, because the field-Noneoutput is byte-identical to wired-and-fresh ([+]config ok). That would re-open the class of #591: an orchestrator routing traffic to a not-yet-configured or stale-watch instance.A router-level unit test now pins the plumbing (a wired probe returning
None→ 503), but it cannot pin the wiring itself.Proposed fix
Make the production constructor fail-closed: default the probe to
|| None("no freshness signal = not ready") inProxyState::new/with_components, and have tests that don't care about readiness opt out explicitly (e.g..with_config_always_fresh()). Then a dropped wiring makes/readyz503 permanently — caught loudly by the harness readiness gate and every e2e — instead of passing silently.Surfaced by the independent audit of #802.