@@ -168,6 +168,9 @@ const getEventKey = (event: StreamEvent | Message, idx: number): string => {
168168 return `message-${ 'id' in event ? ( event as any ) . id : idx } `
169169}
170170
171+ const TIMEOUT_ERROR_MESSAGE =
172+ 'Your connection was interrupted after 30 seconds, possibly due to a Pomerium proxy timeout.'
173+
171174export function Chat ( ) {
172175 const hasMounted = useHasMounted ( )
173176 const messagesEndRef = useRef < HTMLDivElement > ( null )
@@ -181,6 +184,8 @@ export function Chat() {
181184 const [ useWebSearch , setUseWebSearch ] = useState ( false )
182185 const { selectedModel, setSelectedModel } = useModel ( )
183186 const { user } = useUser ( )
187+ const [ requestId , setRequestId ] = useState < string | null > ( null )
188+ const [ timedOut , setTimedOut ] = useState ( false )
184189
185190 const handleModelChange = ( newModel : string ) => {
186191 setSelectedModel ( newModel )
@@ -321,10 +326,15 @@ export function Chat() {
321326 } ,
322327 ] )
323328 setStreaming ( false )
329+ setTimedOut ( false )
324330 } , [ ] )
325331
326332 const handleResponse = useCallback (
327333 ( response : Response ) => {
334+ // Extract x-request-id header for timeout troubleshooting
335+ const xRequestId = response . headers . get ( 'x-request-id' )
336+ setRequestId ( xRequestId )
337+
328338 if ( ! response . ok ) {
329339 console . error (
330340 'Chat response error:' ,
@@ -339,10 +349,12 @@ export function Chat() {
339349 } ,
340350 ] )
341351 setStreaming ( false )
352+ setTimedOut ( false )
342353 return
343354 }
344355
345356 setStreaming ( true )
357+ setTimedOut ( false )
346358
347359 // Clone the response to handle our custom streaming while letting useChat handle its own
348360 const reader = response . clone ( ) . body ?. getReader ( )
@@ -359,12 +371,14 @@ export function Chat() {
359371 } ,
360372 ] )
361373 setStreaming ( false )
374+ setTimedOut ( false )
362375 return
363376 }
364377
365378 const decoder = new TextDecoder ( )
366379 let buffer = ''
367380 let assistantId : string | null = null
381+ let receivedCompletion = false
368382
369383 const processChunk = ( line : string ) => {
370384 if ( line . startsWith ( 'e:' ) ) {
@@ -407,6 +421,11 @@ export function Chat() {
407421 if ( toolState . type === 'tool_call_completed' ) {
408422 return
409423 }
424+ // Handle stream_done event (signals end of stream)
425+ if ( toolState . type === 'stream_done' ) {
426+ receivedCompletion = true
427+ return
428+ }
410429
411430 // Handle reasoning summary streaming
412431 if ( toolState . type === 'reasoning_summary_delta' ) {
@@ -766,6 +785,10 @@ export function Chat() {
766785 // Flush any remaining text buffer
767786 flushTextBuffer ( )
768787 setStreaming ( false )
788+ // If stream ended but we did not receive a completion event, treat as timeout
789+ if ( ! receivedCompletion ) {
790+ setTimedOut ( true )
791+ }
769792 return
770793 }
771794
@@ -848,6 +871,7 @@ export function Chat() {
848871
849872 const handleSendMessage = useCallback (
850873 ( prompt : string ) => {
874+ setTimedOut ( false )
851875 if ( ! hasStartedChat ) {
852876 setHasStartedChat ( true )
853877 }
@@ -1048,6 +1072,40 @@ export function Chat() {
10481072 }
10491073 } ) }
10501074 { streaming && < BotThinking /> }
1075+ { timedOut && (
1076+ < BotError
1077+ key = "timeout-error"
1078+ message = {
1079+ < div className = "grid gap-2" >
1080+ < p > { TIMEOUT_ERROR_MESSAGE } </ p >
1081+ < p >
1082+ See the Pomerium{ ' ' }
1083+ < a
1084+ href = "https://www.pomerium.com/docs/reference/routes/timeouts"
1085+ target = "_blank"
1086+ rel = "noopener noreferrer"
1087+ className = "underline"
1088+ >
1089+ Timeouts Settings documentation
1090+ </ a > { ' ' }
1091+ for more information. .
1092+ </ p >
1093+ { requestId && (
1094+ < >
1095+ < hr />
1096+ < dl >
1097+ < dt > Request ID</ dt >
1098+ < dd > { requestId } </ dd >
1099+ </ dl >
1100+ < p >
1101+ Use this ID to search Pomerium logs for more details.
1102+ </ p >
1103+ </ >
1104+ ) }
1105+ </ div >
1106+ }
1107+ />
1108+ ) }
10511109 < div ref = { messagesEndRef } />
10521110 </ div >
10531111 </ div >
0 commit comments