Skip to main content

ironflow_api/routes/
events.rs

1//! SSE endpoint for real-time event streaming.
2
3use std::convert::Infallible;
4use std::str::FromStr;
5use std::time::Duration;
6
7use axum::extract::{Query, State};
8use axum::response::sse::{Event as SseEvent, KeepAlive, Sse};
9use futures_util::stream::{Stream, StreamExt};
10use serde::Deserialize;
11use serde::de::{self, Deserializer};
12use serde_json::json;
13use tokio::sync::broadcast;
14use tokio_stream::wrappers::BroadcastStream;
15use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
16use uuid::Uuid;
17
18use crate::authorization::AuthorizeExt;
19use crate::error::ApiError;
20use crate::state::AppState;
21use ironflow_auth::extractor::Authenticated;
22use ironflow_engine::notify::Event;
23use ironflow_store::entities::ApiKeyScope;
24
25pub use ironflow_store::entities::EventKind;
26
27/// Deserialize a comma-separated string into `Option<Vec<EventKind>>`.
28fn deserialize_comma_event_kinds<'de, D>(
29    deserializer: D,
30) -> Result<Option<Vec<EventKind>>, D::Error>
31where
32    D: Deserializer<'de>,
33{
34    let opt: Option<String> = Option::deserialize(deserializer)?;
35    match opt {
36        None => Ok(None),
37        Some(raw) => {
38            let kinds: Result<Vec<EventKind>, _> = raw
39                .split(',')
40                .map(|s| s.trim())
41                .filter(|s| !s.is_empty())
42                .map(EventKind::from_str)
43                .collect();
44            kinds.map(Some).map_err(de::Error::custom)
45        }
46    }
47}
48
49/// Query parameters for the SSE events endpoint.
50///
51/// Both fields are optional. When set, only matching events are streamed.
52///
53/// # Examples
54///
55/// ```
56/// use ironflow_api::routes::events::EventsQuery;
57///
58/// let query = EventsQuery {
59///     run_id: None,
60///     types: None,
61/// };
62/// ```
63#[derive(Debug, Deserialize)]
64pub struct EventsQuery {
65    /// Only stream events related to this run.
66    pub run_id: Option<Uuid>,
67    /// Comma-separated list of event types to include (e.g. `?types=run_status_changed,step_completed`).
68    #[serde(default, deserialize_with = "deserialize_comma_event_kinds")]
69    pub types: Option<Vec<EventKind>>,
70}
71
72/// `GET /api/v1/events` -- Server-Sent Events stream.
73///
74/// Streams domain events in real time. Supports optional filtering:
75/// - `?run_id=<uuid>` -- only events for that run
76/// - `?types=run_status_changed,step_completed` -- only those event types
77///
78/// Each SSE message has:
79/// - `event:` set to the event type (e.g. `run_status_changed`)
80/// - `data:` JSON-serialized event payload
81///
82/// A keep-alive comment is sent every 30 seconds.
83///
84/// A client that reads slower than events are published loses the oldest
85/// ones. It then receives a `lagged` event, whatever its filters, with
86/// `data: {"missed": n}`: the number of events it missed. Its view is stale
87/// and it should reload its state.
88///
89/// Provider Account events reach an admin session, or an API key holding the
90/// `admin` scope whose owner is an admin.
91///
92/// # Errors
93///
94/// - 401 if the request is not authenticated
95/// - 403 `INSUFFICIENT_SCOPE` if the API key lacks `runs_read`
96/// - 429 `SSE_CONNECTION_LIMIT` if the caller (user or API key owner) or the
97///   server already holds as many event streams as
98///   [`SseLimits`](crate::sse_limiter::SseLimits) allows
99pub async fn events(
100    auth: Authenticated,
101    State(state): State<AppState>,
102    Query(query): Query<EventsQuery>,
103) -> Result<Sse<impl Stream<Item = Result<SseEvent, Infallible>>>, ApiError> {
104    auth.require(ApiKeyScope::RunsRead, false)?;
105    let connection = state.sse_limiter.try_acquire(auth.user_id)?;
106    let receiver = state.event_sender.subscribe();
107    // Provider Account events describe admin-only resources, which an API
108    // key reaches only through the `admin` scope.
109    let sees_provider_account_events = auth.require(ApiKeyScope::Admin, true).is_ok();
110    let stream = event_stream(
111        receiver,
112        query.run_id,
113        query.types,
114        sees_provider_account_events,
115    );
116    // The stream owns the slot: axum drops it when the client disconnects.
117    let stream = stream.map(move |item| {
118        let _held = &connection;
119        item
120    });
121
122    let sse = Sse::new(stream).keep_alive(KeepAlive::new().interval(Duration::from_secs(30)));
123    Ok(sse)
124}
125
126/// SSE event name sent to a client that fell behind and missed events.
127const LAGGED_EVENT: &str = "lagged";
128
129/// Turn the broadcast `receiver` into the SSE stream a client asked for.
130///
131/// A receiver that falls behind the channel capacity loses the oldest
132/// events: the client gets a [`LAGGED_EVENT`] carrying how many it missed.
133/// That event bypasses the filters, since which events were lost is unknown.
134fn event_stream(
135    receiver: broadcast::Receiver<Event>,
136    run_id_filter: Option<Uuid>,
137    type_filter: Option<Vec<EventKind>>,
138    sees_provider_account_events: bool,
139) -> impl Stream<Item = Result<SseEvent, Infallible>> {
140    BroadcastStream::new(receiver).filter_map(move |result: Result<Event, _>| {
141        let type_filter = type_filter.clone();
142        async move {
143            let event = match result {
144                Ok(event) => event,
145                Err(BroadcastStreamRecvError::Lagged(missed)) => {
146                    let data = json!({ "missed": missed }).to_string();
147                    let sse_event = SseEvent::default().event(LAGGED_EVENT).data(data);
148                    return Some(Ok(sse_event));
149                }
150            };
151
152            if !sees_provider_account_events
153                && matches!(
154                    event,
155                    Event::ProviderAccountUpdated(_) | Event::ProviderAccountUsageUpdated(_)
156                )
157            {
158                return None;
159            }
160
161            if let Some(ref rid) = run_id_filter
162                && event.run_id() != Some(*rid)
163            {
164                return None;
165            }
166
167            if let Some(ref kinds) = type_filter {
168                let event_type = event.event_type();
169                if !kinds.iter().any(|k| k.as_str() == event_type) {
170                    return None;
171                }
172            }
173
174            let data = serde_json::to_string(&event).ok()?;
175            let sse_event = SseEvent::default().event(event.event_type()).data(data);
176
177            Some(Ok::<_, Infallible>(sse_event))
178        }
179    })
180}
181
182#[cfg(test)]
183mod tests {
184    use std::collections::HashMap;
185    use std::sync::Arc;
186    use std::time::Duration;
187
188    use axum::Router;
189    use axum::body::Body;
190    use axum::response::IntoResponse;
191    use axum::response::sse::Sse;
192    use axum::routing::get;
193    use chrono::Utc;
194    use http_body_util::BodyExt;
195    use ironflow_auth::extractor::{API_KEY_PREFIX, API_KEY_SUFFIX_LEN};
196    use ironflow_auth::password;
197    use ironflow_core::providers::claude::ClaudeCodeProvider;
198    use ironflow_engine::engine::Engine;
199    use ironflow_engine::notify::{
200        Event, ProviderAccountChange, ProviderAccountUpdatedEvent, RunStatusChangedEvent,
201        UserSignedInEvent,
202    };
203    use ironflow_store::entities::{ApiKeyScope, NewApiKey, NewUser};
204    use ironflow_store::memory::InMemoryStore;
205    use ironflow_store::models::RunStatus;
206    use rust_decimal::Decimal;
207    use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
208    use tokio::net::{TcpListener, TcpStream};
209    use tokio::sync::broadcast;
210    use tokio::time::{sleep, timeout};
211    use uuid::Uuid;
212
213    use super::{EventKind, event_stream, events};
214    use crate::routes::test_helpers::create_user_auth_header;
215    use crate::sse_limiter::SseLimits;
216    use crate::state::AppState;
217
218    fn test_state() -> AppState {
219        let store = Arc::new(InMemoryStore::new());
220        let provider = Arc::new(ClaudeCodeProvider::new());
221        let engine = Arc::new(Engine::new(store.clone(), provider));
222        let jwt_config = Arc::new(ironflow_auth::jwt::JwtConfig {
223            secret: "test-secret".to_string(),
224            access_token_ttl_secs: 900,
225            refresh_token_ttl_secs: 604800,
226            cookie_domain: None,
227            cookie_secure: false,
228        });
229        let (event_sender, _) = broadcast::channel::<Event>(16);
230        AppState::new(
231            store,
232            engine,
233            jwt_config,
234            "test-worker-token".to_string(),
235            event_sender,
236        )
237    }
238
239    fn sample_run_event(run_id: Uuid) -> Event {
240        Event::RunStatusChanged(RunStatusChangedEvent {
241            run_id,
242            workflow_name: "deploy".to_string(),
243            from: RunStatus::Running,
244            to: RunStatus::Completed,
245            error: None,
246            cost_usd: Decimal::ZERO,
247            duration_ms: 1000,
248            labels: HashMap::new(),
249            at: Utc::now(),
250        })
251    }
252
253    fn sample_user_event() -> Event {
254        Event::UserSignedIn(UserSignedInEvent {
255            user_id: Uuid::now_v7(),
256            username: "alice".to_string(),
257            at: Utc::now(),
258        })
259    }
260
261    fn sample_provider_account_event() -> Event {
262        Event::ProviderAccountUpdated(ProviderAccountUpdatedEvent {
263            account_id: Uuid::now_v7(),
264            name: "acct".into(),
265            change: ProviderAccountChange::Updated,
266            at: Utc::now(),
267        })
268    }
269
270    /// A `Bearer` header for an API key holding `scopes`, owned by a fresh
271    /// user who is an admin when `owner_is_admin` is set.
272    ///
273    /// The key is written to the store directly, so a member may hold scopes
274    /// the `/api-keys` route would refuse them.
275    async fn api_key_header(
276        state: &AppState,
277        owner_is_admin: bool,
278        scopes: Vec<ApiKeyScope>,
279    ) -> String {
280        let suffix = Uuid::now_v7().simple().to_string();
281        let owner = state
282            .store
283            .create_user(NewUser {
284                email: format!("owner-{suffix}@example.com"),
285                username: format!("owner-{suffix}"),
286                password_hash: "argon2hash".to_string(),
287                is_admin: Some(owner_is_admin),
288            })
289            .await
290            .unwrap();
291        let raw_key = format!("irfl_{}rest-of-secret-key", &suffix[24..]);
292        let prefix = &raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
293        state
294            .store
295            .create_api_key(NewApiKey {
296                user_id: owner.id,
297                name: format!("key-{suffix}"),
298                key_hash: password::hash(&raw_key).unwrap(),
299                key_prefix: prefix.to_string(),
300                scopes,
301                expires_at: None,
302                rate_limit_override: None,
303            })
304            .await
305            .unwrap();
306        format!("Bearer {raw_key}")
307    }
308
309    /// Start a real TCP server and return (address, sender, auth header).
310    async fn start_sse_server(state: AppState) -> (String, broadcast::Sender<Event>, String) {
311        let sender = state.event_sender.clone();
312        let auth = create_user_auth_header(&state, "testuser", false).await;
313        let app = Router::new()
314            .route("/events", get(events))
315            .with_state(state);
316
317        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
318        let addr = listener.local_addr().unwrap().to_string();
319        tokio::spawn(async move {
320            axum::serve(listener, app).await.unwrap();
321        });
322        (addr, sender, auth)
323    }
324
325    /// Connect to the SSE endpoint with auth and return a line reader.
326    async fn connect_sse(addr: &str, query: &str, auth: &str) -> BufReader<TcpStream> {
327        let stream = TcpStream::connect(addr).await.unwrap();
328        let (reader, mut writer) = stream.into_split();
329
330        use tokio::io::AsyncWriteExt;
331        writer
332            .write_all(
333                format!(
334                    "GET /events{query} HTTP/1.1\r\nHost: {addr}\r\nAccept: text/event-stream\r\nAuthorization: {auth}\r\n\r\n"
335                )
336                .as_bytes(),
337            )
338            .await
339            .unwrap();
340
341        BufReader::new(reader.reunite(writer).unwrap())
342    }
343
344    /// Read all available data from the SSE stream until `needle` is found
345    /// in the accumulated text, or timeout.
346    async fn read_until_contains(
347        reader: &mut BufReader<TcpStream>,
348        needle: &str,
349        dur: Duration,
350    ) -> String {
351        let mut accumulated = String::new();
352        let result = timeout(dur, async {
353            loop {
354                let mut line = String::new();
355                let n = reader.read_line(&mut line).await.unwrap();
356                if n == 0 {
357                    break;
358                }
359                accumulated.push_str(&line);
360                if accumulated.contains(needle) {
361                    break;
362                }
363            }
364        })
365        .await;
366        if result.is_err() {
367            panic!("timeout waiting for '{needle}' in SSE stream. Data so far:\n{accumulated}");
368        }
369        accumulated
370    }
371
372    /// Wait until the SSE handler has subscribed to the broadcast channel.
373    ///
374    /// Sending before a receiver exists makes `broadcast::Sender::send` return
375    /// `SendError`, so a fixed sleep is racy on slow runners. Poll the receiver
376    /// count instead.
377    async fn wait_for_subscriber(sender: &broadcast::Sender<Event>) {
378        let ready = timeout(Duration::from_secs(5), async {
379            while sender.receiver_count() == 0 {
380                sleep(Duration::from_millis(5)).await;
381            }
382        })
383        .await;
384        assert!(ready.is_ok(), "SSE handler did not subscribe in time");
385    }
386
387    #[tokio::test]
388    async fn sse_stream_receives_events() {
389        let state = test_state();
390        let (addr, sender, auth) = start_sse_server(state).await;
391        let mut reader = connect_sse(&addr, "", &auth).await;
392
393        wait_for_subscriber(&sender).await;
394
395        let run_id = Uuid::now_v7();
396        sender.send(sample_run_event(run_id)).unwrap();
397
398        let text =
399            read_until_contains(&mut reader, &run_id.to_string(), Duration::from_secs(5)).await;
400
401        assert!(text.contains("run_status_changed"));
402        assert!(text.contains(&run_id.to_string()));
403    }
404
405    #[tokio::test]
406    async fn sse_filters_by_run_id() {
407        let state = test_state();
408        let (addr, sender, auth) = start_sse_server(state).await;
409
410        let target_run = Uuid::now_v7();
411        let other_run = Uuid::now_v7();
412
413        let mut reader = connect_sse(&addr, &format!("?run_id={target_run}"), &auth).await;
414        wait_for_subscriber(&sender).await;
415
416        sender.send(sample_run_event(other_run)).unwrap();
417        sender.send(sample_run_event(target_run)).unwrap();
418
419        let text =
420            read_until_contains(&mut reader, &target_run.to_string(), Duration::from_secs(5)).await;
421
422        assert!(text.contains(&target_run.to_string()));
423        assert!(!text.contains(&other_run.to_string()));
424    }
425
426    #[tokio::test]
427    async fn sse_filters_by_event_type() {
428        let state = test_state();
429        let (addr, sender, auth) = start_sse_server(state).await;
430
431        let mut reader = connect_sse(&addr, "?types=user_signed_in", &auth).await;
432        wait_for_subscriber(&sender).await;
433
434        let run_id = Uuid::now_v7();
435        sender.send(sample_run_event(run_id)).unwrap();
436        sender.send(sample_user_event()).unwrap();
437
438        let text = read_until_contains(&mut reader, "user_signed_in", Duration::from_secs(5)).await;
439
440        assert!(text.contains("user_signed_in"));
441        assert!(!text.contains("run_status_changed"));
442    }
443
444    #[tokio::test]
445    async fn sse_returns_correct_content_type() {
446        let state = test_state();
447        let (addr, _sender, auth) = start_sse_server(state).await;
448        let mut reader = connect_sse(&addr, "", &auth).await;
449
450        let text =
451            read_until_contains(&mut reader, "text/event-stream", Duration::from_secs(5)).await;
452
453        assert!(text.contains("text/event-stream"));
454    }
455
456    #[tokio::test]
457    async fn sse_rejects_unauthenticated() {
458        let state = test_state();
459        let (addr, _sender, _auth) = start_sse_server(state).await;
460        // Connect without auth header
461        let stream = TcpStream::connect(&addr).await.unwrap();
462        let (reader, mut writer) = stream.into_split();
463
464        use tokio::io::AsyncWriteExt;
465        writer
466            .write_all(
467                format!(
468                    "GET /events HTTP/1.1\r\nHost: {addr}\r\nAccept: text/event-stream\r\n\r\n"
469                )
470                .as_bytes(),
471            )
472            .await
473            .unwrap();
474
475        let mut buf_reader = BufReader::new(reader.reunite(writer).unwrap());
476        let text = read_until_contains(&mut buf_reader, "401", Duration::from_secs(5)).await;
477
478        assert!(text.contains("401"));
479    }
480
481    /// Stream a provider account event, then a run event, to a client
482    /// authenticated with `auth`, and assert only the run event arrives.
483    ///
484    /// Both events go through the same channel in order: had the provider
485    /// account event not been filtered out, it would arrive first.
486    async fn assert_provider_account_events_hidden(state: AppState, auth: &str) {
487        let (addr, sender, _jwt) = start_sse_server(state).await;
488        let mut reader = connect_sse(&addr, "", auth).await;
489        wait_for_subscriber(&sender).await;
490
491        let run_id = Uuid::now_v7();
492        sender.send(sample_provider_account_event()).unwrap();
493        sender.send(sample_run_event(run_id)).unwrap();
494
495        let text =
496            read_until_contains(&mut reader, &run_id.to_string(), Duration::from_secs(5)).await;
497
498        assert!(!text.contains("provider_account"));
499    }
500
501    #[tokio::test]
502    async fn sse_scope_hides_provider_account_events_from_a_runs_read_key() {
503        let state = test_state();
504        let auth = api_key_header(&state, true, vec![ApiKeyScope::RunsRead]).await;
505        assert_provider_account_events_hidden(state, &auth).await;
506    }
507
508    #[tokio::test]
509    async fn sse_scope_hides_provider_account_events_from_an_admin_scope_key_of_a_member() {
510        let state = test_state();
511        let auth = api_key_header(&state, false, vec![ApiKeyScope::Admin]).await;
512        assert_provider_account_events_hidden(state, &auth).await;
513    }
514
515    #[tokio::test]
516    async fn sse_scope_streams_provider_account_events_to_an_admin_scope_key() {
517        let state = test_state();
518        let auth = api_key_header(&state, true, vec![ApiKeyScope::Admin]).await;
519        let (addr, sender, _jwt) = start_sse_server(state).await;
520        let mut reader = connect_sse(&addr, "", &auth).await;
521        wait_for_subscriber(&sender).await;
522
523        sender.send(sample_provider_account_event()).unwrap();
524
525        let text =
526            read_until_contains(&mut reader, "provider_account", Duration::from_secs(5)).await;
527
528        assert!(text.contains("provider_account"));
529    }
530
531    #[tokio::test]
532    async fn sse_scope_rejects_a_key_without_runs_read() {
533        let state = test_state();
534        let auth = api_key_header(&state, true, vec![ApiKeyScope::StatsRead]).await;
535        let (addr, _sender, _jwt) = start_sse_server(state).await;
536        let mut reader = connect_sse(&addr, "", &auth).await;
537
538        let text = read_until_contains(&mut reader, "\r\n", Duration::from_secs(5)).await;
539
540        assert!(text.starts_with("HTTP/1.1 403"), "status line: {text}");
541    }
542
543    /// A channel of capacity 2 whose receiver already missed 3 of the 5
544    /// events sent: the lag is deterministic, the stream is not polled yet.
545    fn lagging_receiver() -> (broadcast::Sender<Event>, broadcast::Receiver<Event>) {
546        let (sender, receiver) = broadcast::channel::<Event>(2);
547        for _ in 0..5 {
548            sender.send(sample_run_event(Uuid::now_v7())).unwrap();
549        }
550        (sender, receiver)
551    }
552
553    /// Read the SSE body until `needle` shows up, or time out.
554    async fn read_body_until(body: &mut Body, needle: &str) -> String {
555        let mut accumulated = String::new();
556        let result = timeout(Duration::from_secs(5), async {
557            while !accumulated.contains(needle) {
558                let Some(frame) = body.frame().await else {
559                    break;
560                };
561                if let Ok(data) = frame.unwrap().into_data() {
562                    accumulated.push_str(&String::from_utf8_lossy(&data));
563                }
564            }
565        })
566        .await;
567        assert!(
568            result.is_ok(),
569            "timeout waiting for '{needle}'. Data so far:\n{accumulated}"
570        );
571        accumulated
572    }
573
574    #[tokio::test]
575    async fn lagged_event_is_sent_when_the_subscriber_falls_behind() {
576        let (sender, receiver) = lagging_receiver();
577        let response = Sse::new(event_stream(receiver, None, None, false)).into_response();
578        let mut body = response.into_body();
579
580        let text = read_body_until(&mut body, "\"missed\"").await;
581        assert!(text.contains("lagged"), "got {text}");
582        assert!(text.contains("\"missed\":3"), "got {text}");
583
584        // The stream goes on after the notice.
585        let run_id = Uuid::now_v7();
586        sender.send(sample_run_event(run_id)).unwrap();
587        let text = read_body_until(&mut body, &run_id.to_string()).await;
588        assert!(text.contains("run_status_changed"), "got {text}");
589    }
590
591    #[tokio::test]
592    async fn lagged_event_bypasses_the_run_and_type_filters() {
593        let (_sender, receiver) = lagging_receiver();
594        let stream = event_stream(
595            receiver,
596            Some(Uuid::now_v7()),
597            Some(vec![EventKind::UserSignedIn]),
598            false,
599        );
600        let mut body = Sse::new(stream).into_response().into_body();
601
602        let text = read_body_until(&mut body, "\"missed\"").await;
603        assert!(text.contains("lagged"), "got {text}");
604        assert!(!text.contains("run_status_changed"), "got {text}");
605    }
606
607    /// Read the HTTP status line of a response.
608    async fn status_line(reader: &mut BufReader<TcpStream>) -> String {
609        read_until_contains(reader, "\r\n", Duration::from_secs(5)).await
610    }
611
612    /// Read the headers left after the status line, then the body they announce.
613    async fn read_body(reader: &mut BufReader<TcpStream>) -> String {
614        timeout(Duration::from_secs(5), async {
615            let mut content_length = 0;
616            loop {
617                let mut line = String::new();
618                reader.read_line(&mut line).await.unwrap();
619                if line.is_empty() || line == "\r\n" {
620                    break;
621                }
622                if let Some(value) = line.to_ascii_lowercase().strip_prefix("content-length:") {
623                    content_length = value.trim().parse().unwrap();
624                }
625            }
626            let mut body = vec![0; content_length];
627            reader.read_exact(&mut body).await.unwrap();
628            String::from_utf8(body).unwrap()
629        })
630        .await
631        .expect("timeout reading the response body")
632    }
633
634    fn limited_state(per_principal: usize) -> AppState {
635        test_state().with_sse_limits(SseLimits {
636            per_principal: Some(per_principal),
637            global: None,
638        })
639    }
640
641    #[tokio::test]
642    async fn events_returns_429_past_the_per_principal_limit() {
643        let state = limited_state(2);
644        let other_auth = create_user_auth_header(&state, "otheruser", false).await;
645        let (addr, _sender, auth) = start_sse_server(state).await;
646
647        let mut first = connect_sse(&addr, "", &auth).await;
648        assert!(status_line(&mut first).await.starts_with("HTTP/1.1 200"));
649        let mut second = connect_sse(&addr, "", &auth).await;
650        assert!(status_line(&mut second).await.starts_with("HTTP/1.1 200"));
651
652        let mut third = connect_sse(&addr, "", &auth).await;
653        let status = status_line(&mut third).await;
654        assert!(status.starts_with("HTTP/1.1 429"), "status line: {status}");
655        let body = read_body(&mut third).await;
656        assert!(body.contains("SSE_CONNECTION_LIMIT"), "body: {body}");
657
658        let mut other = connect_sse(&addr, "", &other_auth).await;
659        let status = status_line(&mut other).await;
660        assert!(
661            status.starts_with("HTTP/1.1 200"),
662            "another user keeps its own quota: {status}"
663        );
664    }
665
666    #[tokio::test]
667    async fn events_frees_the_slot_when_the_client_disconnects() {
668        let state = limited_state(1);
669        let (addr, _sender, auth) = start_sse_server(state).await;
670
671        let mut first = connect_sse(&addr, "", &auth).await;
672        assert!(status_line(&mut first).await.starts_with("HTTP/1.1 200"));
673        let mut refused = connect_sse(&addr, "", &auth).await;
674        assert!(status_line(&mut refused).await.starts_with("HTTP/1.1 429"));
675
676        drop(first);
677
678        let reopened = timeout(Duration::from_secs(5), async {
679            loop {
680                let mut retry = connect_sse(&addr, "", &auth).await;
681                if status_line(&mut retry).await.starts_with("HTTP/1.1 200") {
682                    return retry;
683                }
684                sleep(Duration::from_millis(20)).await;
685            }
686        })
687        .await;
688        assert!(reopened.is_ok(), "the slot was not freed after disconnect");
689    }
690}