@@ -6,81 +6,37 @@ export type StreamingGuess = {
66/**
77 * Classifies a Response as streaming or non-streaming.
88 *
9- * Uses multiple heuristics :
9+ * Heuristics :
1010 * - No body → not streaming
11- * - Content-Type: text/event-stream → streaming
12- * - Content-Length header present → not streaming
13- * - Otherwise: attempts immediate read with timeout to detect behavior
14- * - Timeout (no data ready) → not streaming (typical SSR/buffered response)
15- * - Stream empty (done) → not streaming
16- * - Got data without Content-Length → streaming (e.g., Vercel AI SDK)
17- * - Got data with Content-Length → not streaming
11+ * - Known streaming Content-Types → streaming (SSE, NDJSON, JSON streaming)
12+ * - text/plain without Content-Length → streaming (some AI APIs)
13+ * - Otherwise → not streaming (conservative default, including HTML/SSR)
1814 *
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- *
22- * Note: Probing will tee() the stream and return a new Response object.
23- *
24- * @param res - The Response to classify
25- * @returns Classification result with safe-to-return Response
15+ * We avoid probing the stream to prevent blocking on transform streams (like injectTraceMetaTags)
16+ * or SSR streams that may not have data ready immediately.
2617 */
27- export async function classifyResponseStreaming ( res : Response ) : Promise < StreamingGuess > {
18+ export function classifyResponseStreaming ( res : Response ) : StreamingGuess {
2819 if ( ! res . body ) {
2920 return { response : res , isStreaming : false } ;
3021 }
3122
3223 const contentType = res . headers . get ( 'content-type' ) ?? '' ;
3324 const contentLength = res . headers . get ( 'content-length' ) ;
3425
35- // Fast path: Server-Sent Events
36- if ( / ^ t e x t \/ e v e n t - s t r e a m \b / i. test ( contentType ) ) {
26+ // Streaming: Known streaming content types
27+ // - text/event-stream: Server-Sent Events (Vercel AI SDK, real-time APIs)
28+ // - application/x-ndjson, application/ndjson: Newline-delimited JSON
29+ // - application/stream+json: JSON streaming
30+ // - text/plain (without Content-Length): Some AI APIs use this for streaming text
31+ if (
32+ / ^ t e x t \/ e v e n t - s t r e a m \b / i. test ( contentType ) ||
33+ / ^ a p p l i c a t i o n \/ ( x - ) ? n d j s o n \b / i. test ( contentType ) ||
34+ / ^ a p p l i c a t i o n \/ s t r e a m \+ j s o n \b / i. test ( contentType ) ||
35+ ( / ^ t e x t \/ p l a i n \b / i. test ( contentType ) && ! contentLength )
36+ ) {
3737 return { response : res , isStreaming : true } ;
3838 }
3939
40- // Fast path: Content-Length indicates buffered response
41- if ( contentLength && / ^ \d + $ / . test ( contentLength ) ) {
42- return { response : res , isStreaming : false } ;
43- }
44-
45- // Probe the stream by trying to read first chunk immediately with a timeout
46- // After tee(), must use the teed stream (original is locked)
47- const [ probeStream , passStream ] = res . body . tee ( ) ;
48- const reader = probeStream . getReader ( ) ;
49-
50- try {
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-
63- reader . releaseLock ( ) ;
64-
65- const teededResponse = new Response ( passStream , res ) ;
66-
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 ) {
74- // Stream completed immediately - buffered (empty body)
75- return { response : teededResponse , isStreaming : false } ;
76- }
77-
78- // Got data immediately without Content-Length - likely streaming
79- // Got data immediately with Content-Length - buffered
80- return { response : teededResponse , isStreaming : contentLength == null } ;
81- } catch {
82- reader . releaseLock ( ) ;
83- // Error reading - treat as non-streaming to be safe
84- return { response : new Response ( passStream , res ) , isStreaming : false } ;
85- }
40+ // Default: treat as non-streaming
41+ return { response : res , isStreaming : false } ;
8642}
0 commit comments