Skip to main content

modo/sse/
last_event_id.rs

1use axum::extract::FromRequestParts;
2use http::request::Parts;
3
4/// Extracts the `Last-Event-ID` header from the request.
5///
6/// When a client reconnects after a disconnect, the browser's `EventSource`
7/// sends a `Last-Event-ID` header with the ID of the last event it received.
8/// Use this extractor to detect reconnections and replay missed events.
9///
10/// Contains `None` on first connection (header absent).
11///
12/// # Replay is application logic
13///
14/// The SSE module does NOT replay events automatically. Your handler is
15/// responsible for fetching missed events from your data store.
16#[derive(Debug, Clone)]
17pub struct LastEventId(pub Option<String>);
18
19impl<S> FromRequestParts<S> for LastEventId
20where
21    S: Send + Sync,
22{
23    type Rejection = std::convert::Infallible;
24
25    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
26        let value = parts
27            .headers
28            .get("last-event-id")
29            .and_then(|v| v.to_str().ok())
30            .map(String::from);
31        Ok(LastEventId(value))
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use axum::extract::FromRequestParts;
39    use http::Request;
40
41    #[tokio::test]
42    async fn extracts_last_event_id_header() {
43        let (mut parts, _body) = Request::builder()
44            .header("last-event-id", "evt_42")
45            .body(())
46            .unwrap()
47            .into_parts();
48        let result = LastEventId::from_request_parts(&mut parts, &()).await;
49        let last_id = result.unwrap();
50        assert_eq!(last_id.0, Some("evt_42".to_string()));
51    }
52
53    #[tokio::test]
54    async fn returns_none_when_header_absent() {
55        let (mut parts, _body) = Request::builder().body(()).unwrap().into_parts();
56        let result = LastEventId::from_request_parts(&mut parts, &()).await;
57        let last_id = result.unwrap();
58        assert_eq!(last_id.0, None);
59    }
60
61    #[tokio::test]
62    async fn non_visible_ascii_header_returns_none() {
63        let (mut parts, _body) = Request::builder().body(()).unwrap().into_parts();
64        parts.headers.insert(
65            "last-event-id",
66            http::HeaderValue::from_bytes(&[0x80]).unwrap(),
67        );
68        let result = LastEventId::from_request_parts(&mut parts, &()).await;
69        let last_id = result.unwrap();
70        assert_eq!(last_id.0, None);
71    }
72}