fix(gateway): fail-closed publish_schema compile + compile-once cache (GW-01 follow-up)
- new gateway::schema_cache — PublishSchemaCache: compile the op's publish_schema once per registration (value-keyed invalidation for hot reload), cache compile failures (logged once at error level, never retried per request) - /publish compile failure is now fail-closed: the chunk stream terminates with INTERNAL (500), the error text stays in the log (no schema internals on the wire) — the per-request warn-and-skip unvalidated ingest path is removed - schema resolution is lazy (first chunk poll, after invoke_sink's 404/403/422 pre-checks — GW-11 order preserved) and keyed by schema value, so re-registration/hot reload is picked up (test) - NdjsonChunkStream: first Err item is terminal (done + stream end), mirroring the wire pump's send(Err) + break — Ok chunks can never follow an error on the HTTP path either (found by spy-handler test) Verified: cargo test (308), cargo test --all-features, clippy --all-targets -D warnings (default + all-features), fmt --check. Tasks: review-001-publish-schema-validation-robust
This commit is contained in:
@@ -127,6 +127,7 @@ impl HttpAdapter {
|
||||
ws_sessions: Arc::clone(&ws_sessions),
|
||||
ws_session_slots: Arc::clone(&ws_session_slots),
|
||||
ws_idle_timeout,
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache::new(),
|
||||
};
|
||||
let router = build_router(state, None);
|
||||
Self {
|
||||
@@ -154,6 +155,7 @@ impl HttpAdapter {
|
||||
ws_sessions: Arc::clone(&self.ws_sessions),
|
||||
ws_session_slots: Arc::clone(&self.ws_session_slots),
|
||||
ws_idle_timeout: self.ws_idle_timeout,
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache::new(),
|
||||
};
|
||||
// `extra_routes` is borrowed, not consumed (SRV-05): a builder
|
||||
// call after `with_extra_routes` must keep the custom routes in
|
||||
@@ -172,6 +174,7 @@ impl HttpAdapter {
|
||||
ws_sessions: Arc::clone(&self.ws_sessions),
|
||||
ws_session_slots: Arc::clone(&self.ws_session_slots),
|
||||
ws_idle_timeout: self.ws_idle_timeout,
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache::new(),
|
||||
};
|
||||
self.router = build_router(state, Some(routes.clone()));
|
||||
self.extra_routes = Some(routes);
|
||||
@@ -195,6 +198,7 @@ impl HttpAdapter {
|
||||
ws_sessions: Arc::clone(&self.ws_sessions),
|
||||
ws_session_slots: Self::rebuild_session_slots(max_sessions),
|
||||
ws_idle_timeout: self.ws_idle_timeout,
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache::new(),
|
||||
};
|
||||
self.router = build_router(state, self.extra_routes.clone());
|
||||
self
|
||||
@@ -217,6 +221,7 @@ impl HttpAdapter {
|
||||
ws_sessions: Arc::clone(&self.ws_sessions),
|
||||
ws_session_slots: Arc::clone(&self.ws_session_slots),
|
||||
ws_idle_timeout: self.ws_idle_timeout,
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache::new(),
|
||||
};
|
||||
self.router = build_router(state, self.extra_routes.clone());
|
||||
self
|
||||
@@ -1025,6 +1030,7 @@ mod tests {
|
||||
crate::websocket::DEFAULT_WS_MAX_SESSIONS,
|
||||
)),
|
||||
ws_idle_timeout: Some(crate::websocket::DEFAULT_WS_IDLE_TIMEOUT),
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ pub(crate) struct RouterState {
|
||||
pub(crate) ws_session_slots: Arc<tokio::sync::Semaphore>,
|
||||
/// Idle-read timeout for the WS pumps (WS-01); `None` disables.
|
||||
pub(crate) ws_idle_timeout: Option<std::time::Duration>,
|
||||
/// Compile-once `publish_schema` validator cache for the `/publish`
|
||||
/// route (GW-01 follow-up); built empty at adapter construction and
|
||||
/// populated lazily per operation.
|
||||
pub(crate) publish_schemas: crate::gateway::schema_cache::PublishSchemaCache,
|
||||
}
|
||||
|
||||
impl axum::extract::FromRef<RouterState> for crate::websocket::SessionState {
|
||||
@@ -102,6 +106,7 @@ mod tests {
|
||||
crate::websocket::DEFAULT_WS_MAX_SESSIONS,
|
||||
)),
|
||||
ws_idle_timeout: Some(crate::websocket::DEFAULT_WS_IDLE_TIMEOUT),
|
||||
publish_schemas: crate::gateway::schema_cache::PublishSchemaCache::new(),
|
||||
};
|
||||
let extracted: DecoyConfig = axum::extract::FromRef::from_ref(&state);
|
||||
assert!(matches!(extracted, DecoyConfig::Redirect { .. }));
|
||||
|
||||
Reference in New Issue
Block a user