aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-02 21:37:48 +0100
committertv <tv@krebsco.de>2026-11-02 21:37:48 +0100
commita6a4b846b34b9829b6ed6ead15f13ea00f73c03d (patch)
tree4cc54dc7eb61b638b024bddc29aa0281d05f2167
parentf2b90bbcdb5347bfb68ee0e3343358e2a8e466dc (diff)
api: /stream & dashboard send X-Accel-Buffering
-rw-r--r--docs/operations.md5
-rw-r--r--src/api/dashboard/mod.rs6
-rw-r--r--src/api/mod.rs7
-rw-r--r--src/api/store.rs5
4 files changed, 22 insertions, 1 deletions
diff --git a/docs/operations.md b/docs/operations.md
index be185f1..7dcdb92 100644
--- a/docs/operations.md
+++ b/docs/operations.md
@@ -125,6 +125,11 @@ location / {
and the node started with `--user-header X-Remote-User`. The names the proxy gives are
the users of `--protocol-writers` and of who wrote what ([`api.md`](api.md)).
+What streams (`/stream`, the dashboard's events) says `X-Accel-Buffering: no`, so nginx
+passes it on as it comes. The dashboard's events have a keepalive every 15 s; `/stream`
+sends nothing while nothing changes, so nginx ends a quiet one after its
+`proxy_read_timeout` (60 s): raise that for it, or have clients reconnect.
+
## What to alert on
From the metrics (`GET /admin/stats` as Prometheus, [`measurements.md`](measurements.md)):
diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs
index 2557906..db7efa3 100644
--- a/src/api/dashboard/mod.rs
+++ b/src/api/dashboard/mod.rs
@@ -619,7 +619,11 @@ async fn events(Extension(hub): Extension<Arc<Hub>>) -> impl IntoResponse {
}
}
};
- Sse::new(stream).keep_alive(KeepAlive::default())
+ (
+ // Through nginx too as it comes, not buffered (`operations.md`).
+ [(super::UNBUFFERED.0, super::UNBUFFERED.1)],
+ Sse::new(stream).keep_alive(KeepAlive::default()),
+ )
}
/// The node stops: the client may reconnect once it is back.
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 9bb2bed..9bda84a 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -110,6 +110,13 @@ pub(crate) fn relay_only_router(
.with_state(relay)
}
+/// On a response that streams: a reverse proxy passes it on as it comes, not once it
+/// has buffered enough (nginx's `X-Accel-Buffering`).
+pub(crate) const UNBUFFERED: (axum::http::HeaderName, &str) = (
+ axum::http::HeaderName::from_static("x-accel-buffering"),
+ "no",
+);
+
/// An error answer: `{"error": message}` with `details`' fields added (`docs/openapi.yaml`,
/// `Error`).
pub(crate) fn error_response(
diff --git a/src/api/store.rs b/src/api/store.rs
index 2b21c83..eac3262 100644
--- a/src/api/store.rs
+++ b/src/api/store.rs
@@ -923,6 +923,11 @@ async fn stream(state: AppState, key: String, path: Vec<String>) -> Result<Respo
axum::http::header::CACHE_CONTROL,
HeaderValue::from_static("no-cache, no-transform"),
);
+ // Through nginx too as it comes, not buffered (`operations.md`).
+ response.headers_mut().insert(
+ super::UNBUFFERED.0,
+ HeaderValue::from_static(super::UNBUFFERED.1),
+ );
Ok(response)
}