diff --git a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs index a031ecdbc2..e9a297de07 100644 --- a/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs +++ b/codex-rs/codex-api/src/endpoint/realtime_websocket/methods.rs @@ -320,30 +320,38 @@ impl RealtimeWebsocketEvents { return Ok(None); } - let msg = match self.rx_message.lock().await.recv().await { - Some(Ok(msg)) => msg, - Some(Err(err)) => { - self.is_closed.store(true, Ordering::SeqCst); - return Err(ApiError::Stream(format!( - "failed to read websocket message: {err}" - ))); - } - None => { - self.is_closed.store(true, Ordering::SeqCst); - return Ok(None); - } - }; + loop { + let msg = match self.rx_message.lock().await.recv().await { + Some(Ok(msg)) => msg, + Some(Err(err)) => { + self.is_closed.store(true, Ordering::SeqCst); + return Err(ApiError::Stream(format!( + "failed to read websocket message: {err}" + ))); + } + None => { + self.is_closed.store(true, Ordering::SeqCst); + return Ok(None); + } + }; - match msg { - Message::Text(text) => Ok(parse_realtime_event(&text)), - Message::Close(_) => { - self.is_closed.store(true, Ordering::SeqCst); - Ok(None) + match msg { + Message::Text(text) => { + if let Some(event) = parse_realtime_event(&text) { + return Ok(Some(event)); + } + } + Message::Close(_) => { + self.is_closed.store(true, Ordering::SeqCst); + return Ok(None); + } + Message::Binary(_) => { + return Ok(Some(RealtimeEvent::Error( + "unexpected binary realtime websocket event".to_string(), + ))); + } + Message::Frame(_) | Message::Ping(_) | Message::Pong(_) => {} } - Message::Binary(_) => Ok(Some(RealtimeEvent::Error( - "unexpected binary realtime websocket event".to_string(), - ))), - Message::Frame(_) | Message::Ping(_) | Message::Pong(_) => Ok(None), } } } diff --git a/codex-rs/codex-api/tests/realtime_websocket_e2e.rs b/codex-rs/codex-api/tests/realtime_websocket_e2e.rs index 69e8572d5f..b9d252d3a8 100644 --- a/codex-rs/codex-api/tests/realtime_websocket_e2e.rs +++ b/codex-rs/codex-api/tests/realtime_websocket_e2e.rs @@ -305,3 +305,89 @@ async fn realtime_ws_e2e_disconnected_emitted_once() { server.await.expect("server task"); } + +#[tokio::test] +async fn realtime_ws_e2e_ignores_unknown_text_events() { + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind"); + let addr = listener.local_addr().expect("local addr"); + + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.expect("accept"); + let mut ws = accept_async(stream).await.expect("accept ws"); + + let first = ws + .next() + .await + .expect("first msg") + .expect("first msg ok") + .into_text() + .expect("text"); + let first_json: Value = serde_json::from_str(&first).expect("json"); + assert_eq!(first_json["type"], "session.create"); + + ws.send(Message::Text( + json!({ + "type": "response.created", + "response": {"id": "resp_unknown"} + }) + .to_string() + .into(), + )) + .await + .expect("send unknown event"); + + ws.send(Message::Text( + json!({ + "type": "session.created", + "session": {"id": "sess_after_unknown"} + }) + .to_string() + .into(), + )) + .await + .expect("send session.created"); + }); + + let provider = Provider { + name: "test".to_string(), + base_url: "http://localhost".to_string(), + query_params: Some(HashMap::new()), + headers: HeaderMap::new(), + retry: RetryConfig { + max_attempts: 1, + base_delay: Duration::from_millis(1), + retry_429: false, + retry_5xx: false, + retry_transport: false, + }, + stream_idle_timeout: Duration::from_secs(5), + }; + let client = RealtimeWebsocketClient::new(provider); + let connection = client + .connect( + RealtimeSessionConfig { + api_url: format!("ws://{addr}"), + prompt: "backend prompt".to_string(), + session_id: Some("conv_123".to_string()), + }, + HeaderMap::new(), + HeaderMap::new(), + ) + .await + .expect("connect"); + + let event = connection + .next_event() + .await + .expect("next event") + .expect("event"); + assert_eq!( + event, + RealtimeEvent::SessionCreated { + session_id: "sess_after_unknown".to_string() + } + ); + + connection.close().await.expect("close"); + server.await.expect("server task"); +}