From 95474fd2d23d8163710f384d54196d9219b66396 Mon Sep 17 00:00:00 2001 From: "joshua.ho@bytedance.com" Date: Wed, 18 Mar 2026 10:12:56 +0800 Subject: [PATCH 1/3] feat(volo-http): add support for sse header validation --- volo-http/src/server/response/sse.rs | 16 ++++++++++++++++ volo-http/src/server/route/router.rs | 27 +++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/volo-http/src/server/response/sse.rs b/volo-http/src/server/response/sse.rs index 3588def7..10949225 100644 --- a/volo-http/src/server/response/sse.rs +++ b/volo-http/src/server/response/sse.rs @@ -20,6 +20,22 @@ use tokio::time::{Instant, Sleep}; use super::IntoResponse; use crate::{body::Body, error::BoxError, response::Response}; +/// Extension trait for [`Response`] to check if it's an SSE response. +pub trait ResponseExt { + /// Check if the response is an SSE response by checking `Content-Type` header. + fn is_sse(&self) -> bool; +} + +impl ResponseExt for Response { + fn is_sse(&self) -> bool { + self.headers() + .get(header::CONTENT_TYPE) + .and_then(|v| v.to_str().ok()) + .map(|v| v.starts_with(mime::TEXT_EVENT_STREAM.essence_str())) + .unwrap_or(false) + } +} + /// Response of [SSE][sse] (Server-Sent Events), inclusing a stream with SSE [`Event`]s. /// /// [sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events diff --git a/volo-http/src/server/route/router.rs b/volo-http/src/server/route/router.rs index 938bc29f..6a9ce4b1 100644 --- a/volo-http/src/server/route/router.rs +++ b/volo-http/src/server/route/router.rs @@ -22,7 +22,7 @@ use crate::{ context::ServerContext, request::Request, response::Response, - server::{IntoResponse, handler::Handler}, + server::{IntoResponse, handler::Handler, response::sse::ResponseExt}, }; /// The router for routing path to [`Service`]s or handlers. @@ -450,7 +450,30 @@ where req: Request, ) -> Result { match self { - Self::MethodRouter(mr) => mr.call(cx, req).await, + Self::MethodRouter(mr) => { + // Check if the client accepts SSE by `Accept` header, true if the header is missing + // or contains `text/event-stream`. + let accepts_sse = req + .headers() + .get(http::header::ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains(mime::TEXT_EVENT_STREAM.essence_str())) + .unwrap_or(true); + + let resp = mr.call(cx, req).await?; + + // If the client does not explicitly accept SSE but the response is SSE, return 415 + // Unsupported Media Type. + if !accepts_sse && resp.is_sse() { + return Ok(Response::builder() + .status(StatusCode::UNSUPPORTED_MEDIA_TYPE) + .body("Not Acceptable".into()) + .unwrap()); + } + + Ok(resp) + } + Self::Service(service) => service.call(cx, req).await, } } From ab69918c3bdea4791ff287870b468b28ca3768a7 Mon Sep 17 00:00:00 2001 From: "joshua.ho@bytedance.com" Date: Fri, 20 Mar 2026 13:08:25 +0800 Subject: [PATCH 2/3] chore(volo-http): improve comments for server sse --- volo-http/src/server/response/sse.rs | 8 ++++---- volo-http/src/server/route/router.rs | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/volo-http/src/server/response/sse.rs b/volo-http/src/server/response/sse.rs index 10949225..a0ffa65e 100644 --- a/volo-http/src/server/response/sse.rs +++ b/volo-http/src/server/response/sse.rs @@ -29,10 +29,10 @@ pub trait ResponseExt { impl ResponseExt for Response { fn is_sse(&self) -> bool { self.headers() - .get(header::CONTENT_TYPE) - .and_then(|v| v.to_str().ok()) - .map(|v| v.starts_with(mime::TEXT_EVENT_STREAM.essence_str())) - .unwrap_or(false) + .get(header::CONTENT_TYPE) // Get the Content-Type header + .and_then(|v| v.to_str().ok()) // Convert header value to &str + .map(|v| v.starts_with(mime::TEXT_EVENT_STREAM.essence_str())) // Check SSE type + .unwrap_or(false) // Return false if header is missing or invalid } } diff --git a/volo-http/src/server/route/router.rs b/volo-http/src/server/route/router.rs index 6a9ce4b1..c55e5b3a 100644 --- a/volo-http/src/server/route/router.rs +++ b/volo-http/src/server/route/router.rs @@ -451,8 +451,8 @@ where ) -> Result { match self { Self::MethodRouter(mr) => { - // Check if the client accepts SSE by `Accept` header, true if the header is missing - // or contains `text/event-stream`. + // Determine if the client accepts SSE by checking the `Accept` header. + // If the header is missing, assume the client accepts SSE (default true). let accepts_sse = req .headers() .get(http::header::ACCEPT) @@ -460,10 +460,11 @@ where .map(|v| v.contains(mime::TEXT_EVENT_STREAM.essence_str())) .unwrap_or(true); + // Call the inner router and get the response. let resp = mr.call(cx, req).await?; - // If the client does not explicitly accept SSE but the response is SSE, return 415 - // Unsupported Media Type. + // If the client does not explicitly accept SSE but the response is SSE, + // return 415 Unsupported Media Type. if !accepts_sse && resp.is_sse() { return Ok(Response::builder() .status(StatusCode::UNSUPPORTED_MEDIA_TYPE) @@ -471,6 +472,7 @@ where .unwrap()); } + // Otherwise, return the response as-is. Ok(resp) } From 83c94e7eb2e8f31d499f325c98191c4c97c22d4f Mon Sep 17 00:00:00 2001 From: "joshua.ho@bytedance.com" Date: Thu, 2 Apr 2026 17:05:58 +0800 Subject: [PATCH 3/3] chore(volo-http): add tests for sse header check --- volo-http/src/server/route/router.rs | 76 +++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/volo-http/src/server/route/router.rs b/volo-http/src/server/route/router.rs index c55e5b3a..7fe5fe5c 100644 --- a/volo-http/src/server/route/router.rs +++ b/volo-http/src/server/route/router.rs @@ -514,14 +514,22 @@ where #[cfg(test)] mod router_tests { + use std::convert::Infallible; + + use async_stream::stream; use faststr::FastStr; - use http::{method::Method, status::StatusCode, uri::Uri}; + use futures::Stream; + use http::{header, method::Method, status::StatusCode, uri::Uri}; use super::Router; use crate::{ body::{Body, BodyConversion}, server::{ - Server, param::PathParamsVec, route::method_router::any, test_helpers::TestServer, + IntoResponse, Request, Server, + param::PathParamsVec, + response::sse::{Event, Sse}, + route::method_router::any, + test_helpers::TestServer, }, }; @@ -839,4 +847,68 @@ mod router_tests { .is_err() ); } + + async fn sse_handler() -> Sse>> { + let stream = stream! { + yield Ok(Event::new().event("ping").data("hello")); + }; + Sse::new(stream) + } + + async fn hello_handler() -> &'static str { + "Hello, World" + } + + async fn get_status( + server: &TestServer>, Option>, + uri: &str, + accept: Option<&'static str>, + ) -> StatusCode { + let mut builder = Request::builder().method(Method::GET).uri(uri); + if let Some(accept) = accept { + builder = builder.header(header::ACCEPT, accept); + } + server + .call_without_cx(builder.body(None).expect("Failed to build request")) + .await + .into_response() + .status() + } + + #[tokio::test] + async fn sse_accepted() { + let router: Router> = Router::new().route("/sse", any(sse_handler)); + let server = Server::new(router).into_test_server(); + assert_eq!( + get_status(&server, "/sse", Some(mime::TEXT_EVENT_STREAM.essence_str())).await, + StatusCode::OK + ); + } + + #[tokio::test] + async fn sse_not_accepted_returns_415() { + let router: Router> = Router::new().route("/sse", any(sse_handler)); + let server = Server::new(router).into_test_server(); + assert_eq!( + get_status(&server, "/sse", Some("application/json")).await, + StatusCode::UNSUPPORTED_MEDIA_TYPE + ); + } + + #[tokio::test] + async fn sse_no_accept_header_defaults_true() { + let router: Router> = Router::new().route("/sse", any(sse_handler)); + let server = Server::new(router).into_test_server(); + assert_eq!(get_status(&server, "/sse", None).await, StatusCode::OK); + } + + #[tokio::test] + async fn non_sse_response_not_blocked() { + let router: Router> = Router::new().route("/hello", any(hello_handler)); + let server = Server::new(router).into_test_server(); + assert_eq!( + get_status(&server, "/hello", Some("application/json")).await, + StatusCode::OK + ); + } }