Skip to main content

gproxy_protocol/adapt/responses_ws/
turn.rs

1use super::*;
2use crate::{
3    codec,
4    connection::WsFrame,
5    transform::generate::stream::responses::ResponsesStreamCollector,
6    wire::{
7        DeclaredFields,
8        openai::responses::websocket::{
9            ClientEvent, ErrorMessage, RequestMessage, ServerEvent, ServerMessage,
10        },
11    },
12};
13use futures_core::Stream;
14use std::{
15    pin::Pin,
16    task::{Context, Poll},
17};
18
19pub struct ResponsesWsTurn<'a> {
20    session: &'a mut ResponsesWsSession,
21    socket: Option<WebSocket>,
22    send: SendState,
23    collector: Option<ResponsesStreamCollector>,
24    received_bytes: usize,
25    received_frames: usize,
26    sent_bytes: usize,
27    sent_frames: usize,
28    terminal: bool,
29    lane: Option<String>,
30    failed: bool,
31}
32
33impl Unpin for ResponsesWsTurn<'_> {}
34enum SendState {
35    Ready(WsFrame),
36    Flush,
37    Done,
38}
39
40impl<'a> ResponsesWsTurn<'a> {
41    pub(super) fn new(
42        session: &'a mut ResponsesWsSession,
43        mut request: RequestMessage,
44    ) -> Result<Self, TransformError> {
45        let ClientEvent::ResponseCreate(body) = &mut request.event else {
46            return Err(TransformError::unsupported(
47                "responses.websocket.control",
48                "steering and injection require a duplex continuation driver, not a single response turn",
49            ));
50        };
51        if body.background.flatten() == Some(true) {
52            return Err(TransformError::unsupported(
53                "responses.websocket.background",
54                "WebSocket mode has no background generation control",
55            ));
56        }
57        body.stream = None;
58        body.background = None;
59        let event = request.into_declared();
60        let lane = event.stream_id.clone();
61        let bytes = codec::encode_json(
62            &event,
63            codec_limits(session.bounds.send_event.min(session.bounds.send_bytes)),
64        )
65        .map_err(|e| codec_error(e, true))?;
66        let len = bytes.len();
67        let frame =
68            WsFrame::Text(String::from_utf8(bytes.to_vec()).expect("JSON serialization is UTF-8"));
69        let collector = ResponsesStreamCollector::new(session.bounds.collector);
70        let socket = session
71            .socket
72            .take()
73            .ok_or_else(|| invalid("socket unavailable"))?;
74        // Closed-before-poll: even mem::forget(turn) cannot reopen this session.
75        session.busy = true;
76        Ok(Self {
77            session,
78            socket: Some(socket),
79            send: SendState::Ready(frame),
80            collector: Some(collector),
81            received_bytes: 0,
82            received_frames: 0,
83            sent_bytes: len,
84            sent_frames: 1,
85            terminal: false,
86            lane,
87            failed: false,
88        })
89    }
90    pub fn received_frames(&self) -> usize {
91        self.received_frames
92    }
93    pub fn received_bytes(&self) -> usize {
94        self.received_bytes
95    }
96    /// Reserved sends, including an uncertain pending readiness/flush operation.
97    pub fn reserved_send_frames(&self) -> usize {
98        self.sent_frames
99    }
100    pub fn reserved_send_bytes(&self) -> usize {
101        self.sent_bytes
102    }
103    pub fn failure_event(&self) -> Option<&StreamEvent> {
104        self.session.failure_event()
105    }
106    pub fn websocket_failure(&self) -> Option<&ErrorMessage> {
107        self.session.websocket_failure()
108    }
109    fn fail(&mut self, error: TransformError) -> TransformError {
110        self.failed = true;
111        self.socket.take();
112        self.session.poison();
113        error
114    }
115    fn account(&mut self, frame: &WsFrame) -> Result<(), TransformError> {
116        self.received_frames = self
117            .received_frames
118            .checked_add(1)
119            .ok_or_else(|| limit("responses.websocket.frames", "receive frame count overflow"))?;
120        let bytes = match frame {
121            WsFrame::Text(v) => v.len(),
122            WsFrame::Binary(v) | WsFrame::Ping(v) | WsFrame::Pong(v) => v.len(),
123            WsFrame::Close(Some(v)) => v
124                .reason
125                .len()
126                .checked_add(2)
127                .ok_or_else(|| limit("responses.websocket.frame", "close payload overflow"))?,
128            WsFrame::Close(None) => 0,
129        };
130        if bytes > self.session.bounds.receive_frame {
131            return Err(limit(
132                "responses.websocket.frame",
133                "receive frame byte cap exceeded",
134            ));
135        }
136        self.received_bytes = self
137            .received_bytes
138            .checked_add(bytes)
139            .filter(|v| *v <= self.session.bounds.receive_bytes)
140            .ok_or_else(|| {
141                limit(
142                    "responses.websocket.bytes",
143                    "aggregate receive byte cap exceeded",
144                )
145            })?;
146        if matches!(
147            frame,
148            WsFrame::Ping(_) | WsFrame::Pong(_) | WsFrame::Close(_)
149        ) && bytes > 125
150        {
151            return Err(invalid("invalid oversized WebSocket control frame"));
152        }
153        Ok(())
154    }
155    fn pong(&mut self, payload: bytes::Bytes) -> Result<(), TransformError> {
156        if payload.len() > self.session.bounds.send_frame {
157            return Err(limit(
158                "responses.websocket.pong",
159                "Pong exceeds host send frame cap",
160            ));
161        }
162        let bytes = self
163            .sent_bytes
164            .checked_add(payload.len())
165            .filter(|v| *v <= self.session.bounds.send_bytes)
166            .ok_or_else(|| {
167                limit(
168                    "responses.websocket.pong",
169                    "Pong exceeds aggregate send byte cap",
170                )
171            })?;
172        let frames = self
173            .sent_frames
174            .checked_add(1)
175            .ok_or_else(|| limit("responses.websocket.pong", "send frame count overflow"))?;
176        self.sent_bytes = bytes;
177        self.sent_frames = frames;
178        self.send = SendState::Ready(WsFrame::Pong(payload));
179        Ok(())
180    }
181    fn decode(&mut self, bytes: &[u8]) -> Result<StreamEvent, TransformError> {
182        if bytes.len() > self.session.bounds.receive_event {
183            return Err(limit(
184                "responses.websocket.event",
185                "JSON event cap exceeded",
186            ));
187        }
188        let limits = codec_limits(self.session.bounds.receive_event);
189        // Known nested WS errors must not fall back to the SSE Error variant
190        // and discard a malformed error object as an unknown extension.
191        let probe: serde_json::Value =
192            codec::decode_json(bytes, limits).map_err(|e| codec_error(e, false))?;
193        if probe.get("type").and_then(serde_json::Value::as_str) == Some("error")
194            && probe.get("error").is_some()
195        {
196            let error = codec::decode_json::<ErrorMessage>(bytes, limits)
197                .map_err(|e| codec_error(e, false))?
198                .into_declared();
199            let lane_matches = error.stream_id.is_none() || error.stream_id == self.lane;
200            self.session.websocket_failure = Some(Box::new(error));
201            return Err(invalid(if lane_matches {
202                "native WebSocket error; see websocket_failure receipt"
203            } else {
204                "WebSocket error belongs to another lane"
205            }));
206        }
207        let message = codec::decode_json::<ServerMessage>(bytes, limits)
208            .map_err(|e| codec_error(e, false))?
209            .into_declared();
210        if message.stream_id != self.lane {
211            return Err(invalid("WebSocket event belongs to another lane"));
212        }
213        let ServerEvent::Response(event) = message.event else {
214            return Err(TransformError::unsupported(
215                "responses.websocket.control",
216                "steering and injection events require a duplex continuation driver",
217            ));
218        };
219        if matches!(event, StreamEvent::Failed(_) | StreamEvent::Error(_)) {
220            self.session.failure = Some(Box::new(event.clone()));
221        }
222        let terminal = matches!(
223            event,
224            StreamEvent::Completed(_) | StreamEvent::Incomplete(_)
225        );
226        self.collector
227            .as_mut()
228            .ok_or_else(|| invalid("collector already consumed"))?
229            .push(event.clone())?;
230        if terminal {
231            let complete = self
232                .collector
233                .take()
234                .ok_or_else(|| invalid("collector already consumed"))?
235                .finish()?;
236            let socket = self
237                .socket
238                .take()
239                .ok_or_else(|| invalid("terminal socket unavailable"))?;
240            self.session.last_response_id = Some(complete.value.id);
241            self.session.last_status = complete.value.status;
242            self.session.last_report = complete.report;
243            self.session.socket = Some(socket);
244            self.session.busy = false;
245            self.terminal = true;
246        }
247        Ok(event)
248    }
249}
250
251impl Stream for ResponsesWsTurn<'_> {
252    type Item = Result<StreamEvent, TransformError>;
253    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
254        let this = self.get_mut();
255        if this.terminal || this.failed {
256            return Poll::Ready(None);
257        }
258        let Some(socket) = this.socket.as_mut() else {
259            return Poll::Ready(Some(Err(this.fail(invalid("socket closed")))));
260        };
261        match poll_send(socket, &mut this.send, cx) {
262            Poll::Pending => return Poll::Pending,
263            Poll::Ready(Err(e)) => return Poll::Ready(Some(Err(this.fail(e)))),
264            Poll::Ready(Ok(())) => {}
265        }
266        if this.received_bytes >= this.session.bounds.receive_bytes {
267            return Poll::Ready(Some(Err(this.fail(limit(
268                "responses.websocket.bytes",
269                "receive byte budget exhausted before terminal",
270            )))));
271        }
272        let frame = match socket.incoming.as_mut().poll_next(cx) {
273            Poll::Pending => return Poll::Pending,
274            Poll::Ready(None) => {
275                return Poll::Ready(Some(Err(
276                    this.fail(invalid("socket ended before terminal response"))
277                )));
278            }
279            Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(this.fail(host_error(e))))),
280            Poll::Ready(Some(Ok(frame))) => frame,
281        };
282        if let Err(e) = this.account(&frame) {
283            return Poll::Ready(Some(Err(this.fail(e))));
284        }
285        match frame {
286            WsFrame::Ping(payload) => {
287                if let Err(e) = this.pong(payload) {
288                    return Poll::Ready(Some(Err(this.fail(e))));
289                }
290                cx.waker().wake_by_ref();
291                Poll::Pending
292            }
293            WsFrame::Pong(_) => {
294                cx.waker().wake_by_ref();
295                Poll::Pending
296            }
297            WsFrame::Close(close) => {
298                this.session.peer_close = close;
299                Poll::Ready(Some(Err(
300                    this.fail(invalid("socket closed before terminal response"))
301                )))
302            }
303            WsFrame::Binary(_) if !this.session.bounds.allow_binary => Poll::Ready(Some(Err(this
304                .fail(TransformError::unsupported(
305                    "responses.websocket.frame",
306                    "binary JSON server messages are disabled",
307                ))))),
308            WsFrame::Text(text) => {
309                Poll::Ready(Some(this.decode(text.as_bytes()).map_err(|e| this.fail(e))))
310            }
311            WsFrame::Binary(bytes) => {
312                Poll::Ready(Some(this.decode(&bytes).map_err(|e| this.fail(e))))
313            }
314        }
315    }
316}
317
318impl Drop for ResponsesWsTurn<'_> {
319    fn drop(&mut self) {
320        if !self.terminal {
321            self.socket.take();
322            self.session.poison();
323        }
324    }
325}
326
327fn poll_send(
328    socket: &mut WebSocket,
329    state: &mut SendState,
330    cx: &mut Context<'_>,
331) -> Poll<Result<(), TransformError>> {
332    loop {
333        match state {
334            SendState::Ready(_) => match socket.outgoing.as_mut().poll_ready(cx) {
335                Poll::Pending => return Poll::Pending,
336                Poll::Ready(Err(e)) => return Poll::Ready(Err(host_error(e))),
337                Poll::Ready(Ok(())) => {
338                    // Move this exact pending frame once. Readiness/flush Pending
339                    // never recreates response.create or replaces a queued Pong.
340                    let SendState::Ready(frame) = std::mem::replace(state, SendState::Flush) else {
341                        // The enclosing arm already matched SendState::Ready.
342                        unreachable!()
343                    };
344                    if let Err(e) = socket.outgoing.as_mut().start_send(frame) {
345                        return Poll::Ready(Err(host_error(e)));
346                    }
347                }
348            },
349            SendState::Flush => match socket.outgoing.as_mut().poll_flush(cx) {
350                Poll::Pending => return Poll::Pending,
351                Poll::Ready(Err(e)) => return Poll::Ready(Err(host_error(e))),
352                Poll::Ready(Ok(())) => *state = SendState::Done,
353            },
354            SendState::Done => return Poll::Ready(Ok(())),
355        }
356    }
357}