Skip to content

ProxyState.config_apply_age defaults fail-open — make the production constructor fail-closed #803

Description

@moonming

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions