@@ -4,7 +4,18 @@ export type StreamingGuess = {
44} ;
55
66/**
7+ * Classifies a Response as streaming or non-streaming.
78 *
9+ * Uses multiple heuristics:
10+ * - Content-Type: text/event-stream → streaming
11+ * - Content-Length header present → not streaming
12+ * - Otherwise: probes stream with timeout to detect behavior
13+ *
14+ * Note: Probing will tee() the stream and return a new Response object.
15+ *
16+ * @param res - The Response to classify
17+ * @param opts.timeoutMs - Probe timeout in ms (default: 25)
18+ * @returns Classification result with safe-to-return Response
819 */
920export async function classifyResponseStreaming (
1021 res : Response ,
@@ -16,57 +27,61 @@ export async function classifyResponseStreaming(
1627 return { response : res , isStreaming : false } ;
1728 }
1829
19- const ct = res . headers . get ( 'content-type' ) ?? '' ;
20- const cl = res . headers . get ( 'content-length' ) ;
30+ const contentType = res . headers . get ( 'content-type' ) ?? '' ;
31+ const contentLength = res . headers . get ( 'content-length' ) ;
2132
22- // Definitive streaming indicators
23- if ( / ^ t e x t \/ e v e n t - s t r e a m \b / i. test ( ct ) ) {
33+ // Fast path: Server-Sent Events
34+ if ( / ^ t e x t \/ e v e n t - s t r e a m \b / i. test ( contentType ) ) {
2435 return { response : res , isStreaming : true } ;
2536 }
2637
27- // Definitive non-streaming indicators
28- if ( cl && / ^ \d + $ / . test ( cl ) ) {
38+ // Fast path: Content-Length indicates buffered response
39+ if ( contentLength && / ^ \d + $ / . test ( contentLength ) ) {
2940 return { response : res , isStreaming : false } ;
3041 }
3142
32- // Probe the stream to detect streaming behavior
33- // NOTE: This tees the stream and returns a new Response object
34- const [ probe , pass ] = res . body . tee ( ) ;
35- const reader = probe . getReader ( ) ;
43+ // Uncertain - probe the stream to determine behavior
44+ // After tee(), must use the teed stream (original is locked)
45+ const [ probeStream , passStream ] = res . body . tee ( ) ;
46+ const reader = probeStream . getReader ( ) ;
3647
37- const firstChunkPromise = ( async ( ) => {
38- try {
39- const { value, done } = await reader . read ( ) ;
40- reader . releaseLock ( ) ;
41- if ( done ) return { arrivedBytes : 0 , done : true } ;
42- const bytes =
43- value && typeof value === 'object' && 'byteLength' in value ? ( value as { byteLength : number } ) . byteLength : 0 ;
44- return { arrivedBytes : bytes , done : false } ;
45- } catch {
46- return { arrivedBytes : 0 , done : false } ;
47- }
48- } ) ( ) ;
48+ const probeResult = await Promise . race ( [
49+ // Try to read first chunk
50+ ( async ( ) => {
51+ try {
52+ const { value, done } = await reader . read ( ) ;
53+ reader . releaseLock ( ) ;
4954
50- const timeout = new Promise < { arrivedBytes : number ; done : boolean } > ( r =>
51- setTimeout ( ( ) => r ( { arrivedBytes : 0 , done : false } ) , timeoutMs ) ,
52- ) ;
55+ if ( done ) {
56+ return { arrivedBytes : 0 , done : true } ;
57+ }
5358
54- const peek = await Promise . race ( [ firstChunkPromise , timeout ] ) ;
59+ const bytes =
60+ value && typeof value === 'object' && 'byteLength' in value
61+ ? ( value as { byteLength : number } ) . byteLength
62+ : 0 ;
63+ return { arrivedBytes : bytes , done : false } ;
64+ } catch {
65+ return { arrivedBytes : 0 , done : false } ;
66+ }
67+ } ) ( ) ,
68+ // Timeout if first chunk takes too long
69+ new Promise < { arrivedBytes : number ; done : boolean } > ( resolve =>
70+ setTimeout ( ( ) => resolve ( { arrivedBytes : 0 , done : false } ) , timeoutMs ) ,
71+ ) ,
72+ ] ) ;
5573
56- // We must return the teed response since original is now locked
57- const preserved = new Response ( pass , res ) ;
74+ const teededResponse = new Response ( passStream , res ) ;
5875
59- let isStreaming = false ;
60- if ( peek . done ) {
61- // Stream completed immediately
62- isStreaming = false ;
63- } else if ( peek . arrivedBytes === 0 ) {
64- // Timeout waiting for first chunk - definitely streaming
65- isStreaming = true ;
76+ // Determine if streaming based on probe result
77+ if ( probeResult . done ) {
78+ // Stream completed immediately - buffered
79+ return { response : teededResponse , isStreaming : false } ;
80+ } else if ( probeResult . arrivedBytes === 0 ) {
81+ // Timeout waiting - definitely streaming
82+ return { response : teededResponse , isStreaming : true } ;
6683 } else {
67- // Got first chunk - streaming if no Content-Length
68- isStreaming = cl == null ;
84+ // Got chunk quickly - streaming if no Content-Length
85+ return { response : teededResponse , isStreaming : contentLength == null } ;
6986 }
70-
71- return { response : preserved , isStreaming } ;
7287}
0 commit comments