fix(websocket): configurable WS read idle timeout (WS-01)

This commit is contained in:
2026-08-29 13:45:54 +00:00
parent fa73684ebe
commit 25975ac2a8
6 changed files with 241 additions and 15 deletions
+30
View File
@@ -90,6 +90,7 @@ pub struct HttpAdapter {
ws_sessions: Arc<crate::websocket::WsSessions>,
ws_max_sessions: usize,
ws_session_slots: Arc<tokio::sync::Semaphore>,
ws_idle_timeout: Option<Duration>,
}
impl HttpAdapter {
@@ -117,6 +118,7 @@ impl HttpAdapter {
let ws_sessions = Arc::new(crate::websocket::WsSessions::new());
let ws_max_sessions = crate::websocket::DEFAULT_WS_MAX_SESSIONS;
let ws_session_slots = Arc::new(tokio::sync::Semaphore::new(ws_max_sessions));
let ws_idle_timeout = Some(crate::websocket::DEFAULT_WS_IDLE_TIMEOUT);
let state = RouterState {
registry: Arc::clone(&registry),
identity_provider: Arc::clone(&identity_provider),
@@ -124,6 +126,7 @@ impl HttpAdapter {
openapi_doc: openapi_doc.clone(),
ws_sessions: Arc::clone(&ws_sessions),
ws_session_slots: Arc::clone(&ws_session_slots),
ws_idle_timeout,
};
let router = build_router(state, None);
Self {
@@ -137,6 +140,7 @@ impl HttpAdapter {
ws_sessions,
ws_max_sessions,
ws_session_slots,
ws_idle_timeout,
}
}
@@ -149,6 +153,7 @@ impl HttpAdapter {
openapi_doc: self.openapi_doc.clone(),
ws_sessions: Arc::clone(&self.ws_sessions),
ws_session_slots: Arc::clone(&self.ws_session_slots),
ws_idle_timeout: self.ws_idle_timeout,
};
// `extra_routes` is borrowed, not consumed (SRV-05): a builder
// call after `with_extra_routes` must keep the custom routes in
@@ -166,6 +171,7 @@ impl HttpAdapter {
openapi_doc: self.openapi_doc.clone(),
ws_sessions: Arc::clone(&self.ws_sessions),
ws_session_slots: Arc::clone(&self.ws_session_slots),
ws_idle_timeout: self.ws_idle_timeout,
};
self.router = build_router(state, Some(routes.clone()));
self.extra_routes = Some(routes);
@@ -188,6 +194,29 @@ impl HttpAdapter {
openapi_doc: self.openapi_doc.clone(),
ws_sessions: Arc::clone(&self.ws_sessions),
ws_session_slots: Self::rebuild_session_slots(max_sessions),
ws_idle_timeout: self.ws_idle_timeout,
};
self.router = build_router(state, self.extra_routes.clone());
self
}
/// The WS idle-read timeout (WS-01): the read pump closes the
/// connection with a 1001 (GoingAway) close frame after this long
/// without an inbound WS message — bounding the demux stall a
/// dribbling (or silently-stalled) peer can pin. `None` disables
/// the knob (not recommended: the stall window is then unbounded).
///
/// Default: [`crate::websocket::DEFAULT_WS_IDLE_TIMEOUT`] (60 s).
pub fn with_ws_idle_timeout(mut self, idle_timeout: Option<Duration>) -> Self {
self.ws_idle_timeout = idle_timeout;
let state = RouterState {
registry: Arc::clone(&self.registry),
identity_provider: Arc::clone(&self.identity_provider),
decoy: self.decoy.clone(),
openapi_doc: self.openapi_doc.clone(),
ws_sessions: Arc::clone(&self.ws_sessions),
ws_session_slots: Arc::clone(&self.ws_session_slots),
ws_idle_timeout: self.ws_idle_timeout,
};
self.router = build_router(state, self.extra_routes.clone());
self
@@ -995,6 +1024,7 @@ mod tests {
ws_session_slots: Arc::new(tokio::sync::Semaphore::new(
crate::websocket::DEFAULT_WS_MAX_SESSIONS,
)),
ws_idle_timeout: Some(crate::websocket::DEFAULT_WS_IDLE_TIMEOUT),
}
}