1- import { cloneRequest } from './utils' ;
1+ import { cloneRequest , teeRequest } from './utils' ;
22import { FirewallInterferenceError } from './firewall-interference-error' ;
33
44const 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+
629export 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