@@ -10,11 +10,15 @@ export type StreamingGuess = {
1010 * - No body → not streaming
1111 * - Content-Type: text/event-stream → streaming
1212 * - Content-Length header present → not streaming
13- * - Otherwise: attempts immediate read to detect behavior
13+ * - Otherwise: attempts immediate read with timeout to detect behavior
14+ * - Timeout (no data ready) → not streaming (typical SSR/buffered response)
1415 * - Stream empty (done) → not streaming
15- * - Got data without Content-Length → streaming
16+ * - Got data without Content-Length → streaming (e.g., Vercel AI SDK)
1617 * - Got data with Content-Length → not streaming
1718 *
19+ * The timeout prevents blocking on responses that are being generated (like SSR),
20+ * while still detecting true streaming responses that produce data immediately.
21+ *
1822 * Note: Probing will tee() the stream and return a new Response object.
1923 *
2024 * @param res - The Response to classify
@@ -38,23 +42,41 @@ export async function classifyResponseStreaming(res: Response): Promise<Streamin
3842 return { response : res , isStreaming : false } ;
3943 }
4044
41- // Probe the stream by trying to read first chunk immediately
45+ // Probe the stream by trying to read first chunk immediately with a timeout
4246 // After tee(), must use the teed stream (original is locked)
4347 const [ probeStream , passStream ] = res . body . tee ( ) ;
4448 const reader = probeStream . getReader ( ) ;
4549
4650 try {
47- const { done } = await reader . read ( ) ;
51+ // Use a short timeout to avoid blocking on responses that aren't immediately ready
52+ // Streaming responses (like Vercel AI) typically start producing data right away
53+ // Buffered responses (like Remix SSR) will block until content is generated
54+ const PROBE_TIMEOUT_MS = 10 ;
55+
56+ const timeoutPromise = new Promise < { done : boolean ; value ?: unknown ; timedOut : true } > ( resolve => {
57+ setTimeout ( ( ) => resolve ( { done : false , value : undefined , timedOut : true } ) , PROBE_TIMEOUT_MS ) ;
58+ } ) ;
59+
60+ const readPromise = reader . read ( ) . then ( result => ( { ...result , timedOut : false as const } ) ) ;
61+ const result = await Promise . race ( [ readPromise , timeoutPromise ] ) ;
62+
4863 reader . releaseLock ( ) ;
4964
5065 const teededResponse = new Response ( passStream , res ) ;
5166
52- if ( done ) {
67+ if ( result . timedOut ) {
68+ // Timeout means data isn't immediately available - likely a buffered response
69+ // being generated (like SSR). Treat as non-streaming.
70+ return { response : teededResponse , isStreaming : false } ;
71+ }
72+
73+ if ( result . done ) {
5374 // Stream completed immediately - buffered (empty body)
5475 return { response : teededResponse , isStreaming : false } ;
5576 }
5677
57- // Got data - treat as streaming if no Content-Length header
78+ // Got data immediately without Content-Length - likely streaming
79+ // Got data immediately with Content-Length - buffered
5880 return { response : teededResponse , isStreaming : contentLength == null } ;
5981 } catch {
6082 reader . releaseLock ( ) ;
0 commit comments