Skip to content

Commit 4367a2f

Browse files
committed
Only buffer request body on browsers without ReadableStream upload support
Instead of unconditionally buffering the request body into an ArrayBuffer for all browsers, detect at runtime whether the browser supports ReadableStream as a fetch() request body. Use the streaming teeRequest() approach when supported (Chrome, Firefox) and fall back to ArrayBuffer buffering only when not (Safari). Addresses review feedback to avoid degrading streaming performance for browsers that already support it. Made-with: Cursor
1 parent ac46d66 commit 4367a2f

File tree

1 file changed

+70
-15
lines changed

1 file changed

+70
-15
lines changed

packages/php-wasm/web-service-worker/src/fetch-with-cors-proxy.ts

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
1-
import { cloneRequest } from './utils';
1+
import { cloneRequest, teeRequest } from './utils';
22
import { FirewallInterferenceError } from './firewall-interference-error';
33

44
const CORS_PROXY_HEADER = 'X-Playground-Cors-Proxy';
55

6+
let streamBodySupported: boolean | undefined;
7+
async function supportsReadableStreamBody(): Promise<boolean> {
8+
if (streamBodySupported !== undefined) {
9+
return streamBodySupported;
10+
}
11+
try {
12+
const stream = new ReadableStream({
13+
start(controller) {
14+
controller.close();
15+
},
16+
});
17+
await fetch('data:a/a,', {
18+
method: 'POST',
19+
body: stream,
20+
duplex: 'half',
21+
} as RequestInit);
22+
streamBodySupported = true;
23+
} catch {
24+
streamBodySupported = false;
25+
}
26+
return streamBodySupported;
27+
}
28+
629
export async function fetchWithCorsProxy(
730
input: RequestInfo,
831
init?: RequestInit,
@@ -54,22 +77,34 @@ export async function fetchWithCorsProxy(
5477
return await fetch(requestObject);
5578
}
5679

57-
/**
58-
* Buffer the request body so it can be reused across the direct fetch
59-
* attempt and the CORS proxy fallback. We buffer into an ArrayBuffer
60-
* instead of using ReadableStream.tee() because Safari does not support
61-
* ReadableStream as a fetch() request body ("ReadableStream uploading
62-
* is not supported").
63-
*/
64-
let bufferedBody: ArrayBuffer | null = null;
65-
if (requestObject.body) {
66-
bufferedBody = await new Response(requestObject.body).arrayBuffer();
80+
const useStreaming = await supportsReadableStreamBody();
81+
82+
let directRequest: Request;
83+
let corsProxyBody: ArrayBuffer | ReadableStream<Uint8Array> | null;
84+
85+
if (useStreaming) {
86+
const [teedDirect, teedProxy] = await teeRequest(requestObject);
87+
directRequest = teedDirect;
88+
corsProxyBody = teedProxy.body;
89+
} else {
90+
/**
91+
* Buffer the request body so it can be reused across the direct
92+
* fetch attempt and the CORS proxy fallback. Safari does not
93+
* support ReadableStream as a fetch() request body
94+
* ("ReadableStream uploading is not supported").
95+
*/
96+
let bufferedBody: ArrayBuffer | null = null;
97+
if (requestObject.body) {
98+
bufferedBody = await new Response(requestObject.body).arrayBuffer();
99+
}
100+
directRequest = await cloneRequest(requestObject, {
101+
body: bufferedBody,
102+
});
103+
corsProxyBody = bufferedBody;
67104
}
68105

69106
try {
70-
return await fetch(
71-
await cloneRequest(requestObject, { body: bufferedBody })
72-
);
107+
return await fetch(directRequest);
73108
} catch {
74109
const headers = new Headers(requestObject.headers);
75110
const corsProxyAllowedHeaders =
@@ -93,10 +128,30 @@ export async function fetchWithCorsProxy(
93128
headers.set('content-type', 'application/octet-stream');
94129
}
95130

131+
/**
132+
* When using streaming bodies, buffer into an ArrayBuffer if the
133+
* CORS proxy uses `http:`. Streaming request bodies cause Chrome
134+
* to silently upgrade HTTP/1.1 to HTTP/2, but an HTTP/1.1-only
135+
* server replies with HTTP/1.1, triggering ERR_ALPN_NEGOTIATION_FAILED.
136+
*
137+
* @see https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests
138+
*/
139+
let body = corsProxyBody;
140+
if (useStreaming && body) {
141+
const rootUrl = new URL(import.meta.url);
142+
rootUrl.pathname = '';
143+
rootUrl.search = '';
144+
rootUrl.hash = '';
145+
const corsProxyUrlObj = new URL(corsProxyUrl, rootUrl.toString());
146+
if (corsProxyUrlObj.protocol === 'http:') {
147+
body = await new Response(body).arrayBuffer();
148+
}
149+
}
150+
96151
const newRequest = await cloneRequest(requestObject, {
97152
url: `${corsProxyUrl}${requestObject.url}`,
98153
headers,
99-
body: bufferedBody,
154+
body,
100155
...(requestIntendsToPassCredentials && { credentials: 'include' }),
101156
});
102157

0 commit comments

Comments
 (0)