cratestack_axum/rpc/
sse.rs1use std::pin::Pin;
30
31use axum::body::{Body, Bytes};
32use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
33use axum::response::Response;
34use cratestack_core::CratestackError;
35use cratestack_core::rpc::RpcErrorBody;
36use futures_util::Stream;
37use futures_util::stream::{self, StreamExt};
38use serde::Serialize;
39
40use crate::transport::StreamedResponseMarker;
41
42const SSE_CONTENT_TYPE: &str = "text/event-stream";
43
44pub fn validate_subscribe_accept_header(headers: &HeaderMap) -> Result<(), CratestackError> {
50 let Some(accept) = headers.get(header::ACCEPT) else {
51 return Err(CratestackError::NotAcceptable(format!(
52 "subscription endpoint requires Accept: {SSE_CONTENT_TYPE}"
53 )));
54 };
55 let accept = accept
56 .to_str()
57 .map_err(|error| CratestackError::BadRequest(format!("invalid Accept header: {error}")))?;
58 if accept
59 .split(',')
60 .map(str::trim)
61 .any(|value| value == SSE_CONTENT_TYPE || value == "*/*")
62 {
63 Ok(())
64 } else {
65 Err(CratestackError::NotAcceptable(format!(
66 "subscription endpoint requires Accept: {SSE_CONTENT_TYPE}, got {accept}"
67 )))
68 }
69}
70
71pub fn encode_model_event_sse_response<T, S>(items: S) -> Response
76where
77 T: Serialize + Send + 'static,
78 S: Stream<Item = T> + Send + 'static,
79{
80 sse_response(Body::from_stream(encode_sse_events(items)))
81}
82
83pub(super) fn sse_response(body: Body) -> Response {
85 let mut response = Response::new(body);
86 *response.status_mut() = StatusCode::OK;
87 response.headers_mut().insert(
88 header::CONTENT_TYPE,
89 HeaderValue::from_static(SSE_CONTENT_TYPE),
90 );
91 response
92 .headers_mut()
93 .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
94 response.extensions_mut().insert(StreamedResponseMarker);
99 response
100}
101
102struct EventState<S> {
103 source: Pin<Box<S>>,
104 id: u64,
105 ended: bool,
106}
107
108fn encode_sse_events<T, S>(
109 items: S,
110) -> impl Stream<Item = Result<Bytes, std::convert::Infallible>> + Send
111where
112 T: Serialize + Send + 'static,
113 S: Stream<Item = T> + Send + 'static,
114{
115 let initial = EventState {
116 source: Box::pin(items),
117 id: 0,
118 ended: false,
119 };
120 stream::unfold(initial, |mut state| async move {
121 if state.ended {
122 return None;
123 }
124 match state.source.next().await {
125 Some(item) => {
126 state.id += 1;
127 let bytes = format_message_event(state.id, &item);
128 Some((Ok(bytes), state))
129 }
130 None => {
131 state.ended = true;
132 state.id += 1;
133 let bytes = format_error_event(state.id, &lagged_error());
134 Some((Ok(bytes), state))
135 }
136 }
137 })
138}
139
140fn lagged_error() -> RpcErrorBody {
141 RpcErrorBody::from_cratestack(&CratestackError::Unavailable(
142 "subscription lagged".to_owned(),
143 ))
144}
145
146#[derive(Serialize)]
147struct SseStreamItemPayload<'a, T> {
148 id: u64,
149 next: &'a T,
150}
151
152#[derive(Serialize)]
153struct SseErrorPayload<'a> {
154 id: u64,
155 err: &'a RpcErrorBody,
156}
157
158pub(super) fn format_message_event<T: Serialize>(id: u64, item: &T) -> Bytes {
159 let payload = SseStreamItemPayload { id, next: item };
160 let json =
161 serde_json::to_string(&payload).unwrap_or_else(|_| r#"{"id":0,"next":null}"#.to_owned());
162 Bytes::from(format!("event: message\ndata: {json}\n\n"))
163}
164
165pub(super) fn format_error_event(id: u64, error: &RpcErrorBody) -> Bytes {
166 let payload = SseErrorPayload { id, err: error };
167 let json = serde_json::to_string(&payload).unwrap_or_else(|_| {
168 r#"{"id":0,"err":{"code":"internal","message":"encode failure"}}"#.to_owned()
169 });
170 Bytes::from(format!("event: error\ndata: {json}\n\n"))
171}
172
173#[cfg(test)]
174mod tests;