commit d4a353f8d326e863fede66c19b068dbe222bce18
parent 151a4d25eb0556ee8495868e0ff5abd529c7b565
Author: Silas Brack <silasbrack@gmail.com>
Date: Thu, 11 Jun 2026 14:55:03 +0200
fix: add X-Accel-Buffering header to SSE responses for nginx
Nginx buffers proxy responses by default, which prevents SSE
events from reaching the client in real-time. The /api/ location
had explicit proxy_buffering=off but the / location (serving the
app) did not. Adding X-Accel-Buffering: no to all SSE responses
tells nginx to disable buffering per-response.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/src/handlers/story.rs b/src/handlers/story.rs
@@ -3,7 +3,7 @@ use std::convert::Infallible;
use askama::Template;
use axum::{
extract::{Path, State},
- response::{Html, Redirect, sse::{Event, Sse}},
+ response::{Html, IntoResponse, Redirect, Response, sse::{Event, Sse}},
Form,
};
use axum_extra::extract::cookie::CookieJar;
@@ -407,7 +407,7 @@ pub async fn comments_stream(
State(state): State<AppState>,
jar: CookieJar,
Path(story_id): Path<i64>,
-) -> Sse<impl tokio_stream::Stream<Item = Result<Event, Infallible>>> {
+) -> Response {
let tokens = get_tokens_from_cookies(&jar);
let (tx, rx) = tokio::sync::mpsc::channel::<Result<Event, Infallible>>(10);
@@ -492,10 +492,15 @@ pub async fn comments_stream(
});
let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
- Sse::new(stream).keep_alive(
+ let sse = Sse::new(stream).keep_alive(
axum::response::sse::KeepAlive::new()
.interval(std::time::Duration::from_secs(30)),
- )
+ );
+ let mut response = sse.into_response();
+ response
+ .headers_mut()
+ .insert("X-Accel-Buffering", "no".parse().unwrap());
+ response
}
fn render_edit_error(story_id: i64, form: &EditForm, all_tags: &[trailbase::Category], error: &str) -> Html<String> {
diff --git a/src/sse.rs b/src/sse.rs
@@ -26,6 +26,7 @@ fn patch_elements_with_options(
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/event-stream")
.header(header::CACHE_CONTROL, "no-cache")
+ .header("X-Accel-Buffering", "no")
.body(Body::from(body))
.unwrap()
}