Skip to content

Commit ca4bafa

Browse files
committed
give this a shot
1 parent 62a1b2c commit ca4bafa

File tree

1 file changed

+28
-6
lines changed

1 file changed

+28
-6
lines changed

packages/cloudflare/src/utils/streaming.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)