44 * SPDX-License-Identifier: MIT
55 */
66
7+ #if FETCH_STREAMING
8+ /**
9+ * A class that mimics the XMLHttpRequest API using the modern Fetch API.
10+ * This implementation is specifically tailored to only handle 'arraybuffer'
11+ * responses.
12+ */
13+ // TODO Use a regular class name when #5840 is fixed.
14+ var FetchXHR = class {
15+ // --- Public XHR Properties ---
16+
17+ // Event Handlers
18+ onload = null ;
19+ onerror = null ;
20+ onprogress = null ;
21+ onreadystatechange = null ;
22+ ontimeout = null ;
23+
24+ // Request Configuration
25+ responseType = 'arraybuffer' ;
26+ withCredentials = false ;
27+ timeout = 0 ; // Standard XHR timeout property
28+
29+ // Response / State Properties
30+ readyState = 0 ; // 0: UNSENT
31+ response = null ;
32+ responseURL = '' ;
33+ status = 0 ;
34+ statusText = '' ;
35+
36+ // --- Internal Properties ---
37+ _method = '' ;
38+ _url = '' ;
39+ _headers = { } ;
40+ _abortController = null ;
41+ _aborted = false ;
42+ _responseHeaders = null ;
43+
44+ // --- Private state management ---
45+ _changeReadyState ( state ) {
46+ this . readyState = state ;
47+ this . onreadystatechange ?. ( ) ;
48+ }
49+
50+ // --- Public XHR Methods ---
51+
52+ /**
53+ * Initializes a request.
54+ * @param {string } method The HTTP request method (e.g., 'GET', 'POST').
55+ * @param {string } url The URL to send the request to.
56+ * @param {boolean } [async=true] This parameter is ignored as Fetch is always async.
57+ * @param {string|null } [user=null] The username for basic authentication.
58+ * @param {string|null } [password=null] The password for basic authentication.
59+ */
60+ open ( method , url , async = true , user = null , password = null ) {
61+ if ( this . readyState !== 0 && this . readyState !== 4 ) {
62+ console . warn ( "FetchXHR.open() called while a request is in progress." ) ;
63+ this . abort ( ) ;
64+ }
65+
66+ // Reset internal state for the new request
67+ this . _method = method ;
68+ this . _url = url ;
69+ this . _headers = { } ;
70+ this . _responseHeaders = null ;
71+
72+ // The async parameter is part of the XHR API but is an error here because
73+ // the Fetch API is inherently asynchronous and does not support synchronous requests.
74+ if ( ! async ) {
75+ throw new Error ( "FetchXHR does not support synchronous requests." ) ;
76+ }
77+
78+ // Handle Basic Authentication if user/password are provided.
79+ // This creates a base64-encoded string and sets the Authorization header.
80+ if ( user ) {
81+ const credentials = btoa ( `${ user } :${ password || '' } ` ) ;
82+ this . _headers [ 'Authorization' ] = `Basic ${ credentials } ` ;
83+ }
84+
85+ this . _changeReadyState ( 1 ) ; // 1: OPENED
86+ }
87+
88+ /**
89+ * Sets the value of an HTTP request header.
90+ * @param {string } header The name of the header.
91+ * @param {string } value The value of the header.
92+ */
93+ setRequestHeader ( header , value ) {
94+ if ( this . readyState !== 1 ) {
95+ throw new Error ( 'setRequestHeader can only be called when state is OPENED.' ) ;
96+ }
97+ this . _headers [ header ] = value ;
98+ }
99+
100+ /**
101+ * This method is not effectively implemented because Fetch API relies on the
102+ * server's Content-Type header and does not support overriding the MIME type
103+ * on the client side in the same way as XHR.
104+ * @param {string } mimetype The MIME type to use.
105+ */
106+ overrideMimeType ( mimetype ) {
107+ throw new Error ( "overrideMimeType is not supported by the Fetch API and has no effect." ) ;
108+ }
109+
110+ /**
111+ * Returns a string containing all the response headers, separated by CRLF.
112+ * @returns {string } The response headers.
113+ */
114+ getAllResponseHeaders ( ) {
115+ if ( ! this . _responseHeaders ) {
116+ return '' ;
117+ }
118+
119+ let headersString = '' ;
120+ // The Headers object is iterable.
121+ for ( const [ key , value ] of this . _responseHeaders . entries ( ) ) {
122+ headersString += `${ key } : ${ value } \r\n` ;
123+ }
124+ return headersString ;
125+ }
126+
127+ /**
128+ * Sends the request.
129+ * @param body The body of the request.
130+ */
131+ async send ( body = null ) {
132+ if ( this . readyState !== 1 ) {
133+ throw new Error ( 'send() can only be called when state is OPENED.' ) ;
134+ }
135+
136+ this . _abortController = new AbortController ( ) ;
137+ const signal = this . _abortController . signal ;
138+
139+ // Handle timeout
140+ let timeoutID ;
141+ if ( this . timeout > 0 ) {
142+ timeoutID = setTimeout (
143+ ( ) => this . _abortController . abort ( new DOMException ( 'The user aborted a request.' , 'TimeoutError' ) ) ,
144+ this . timeout
145+ ) ;
146+ }
147+
148+ const fetchOptions = {
149+ method : this . _method ,
150+ headers : this . _headers ,
151+ body : body ,
152+ signal : signal ,
153+ credentials : this . withCredentials ? 'include' : 'same-origin' ,
154+ } ;
155+
156+ try {
157+ const response = await fetch ( this . _url , fetchOptions ) ;
158+
159+ // Populate response properties once headers are received
160+ this . status = response . status ;
161+ this . statusText = response . statusText ;
162+ this . responseURL = response . url ;
163+ this . _responseHeaders = response . headers ;
164+ this . _changeReadyState ( 2 ) ; // 2: HEADERS_RECEIVED
165+
166+ // Start processing the body
167+ this . _changeReadyState ( 3 ) ; // 3: LOADING
168+
169+ if ( ! response . body ) {
170+ throw new Error ( "Response has no body to read." ) ;
171+ }
172+
173+ const reader = response . body . getReader ( ) ;
174+ const contentLength = + response . headers . get ( 'Content-Length' ) ;
175+
176+ let receivedLength = 0 ;
177+ const chunks = [ ] ;
178+
179+ while ( true ) {
180+ const { done, value } = await reader . read ( ) ;
181+ if ( done ) {
182+ break ;
183+ }
184+
185+ chunks . push ( value ) ;
186+ receivedLength += value . length ;
187+
188+ if ( this . onprogress ) {
189+ // Convert to ArrayBuffer as requested by responseType.
190+ this . response = value . buffer ;
191+ const progressEvent = {
192+ lengthComputable : contentLength > 0 ,
193+ loaded : receivedLength ,
194+ total : contentLength
195+ } ;
196+ this . onprogress ( progressEvent ) ;
197+ }
198+ }
199+
200+ // Combine chunks into a single Uint8Array.
201+ const allChunks = new Uint8Array ( receivedLength ) ;
202+ let position = 0 ;
203+ for ( const chunk of chunks ) {
204+ allChunks . set ( chunk , position ) ;
205+ position += chunk . length ;
206+ }
207+
208+ // Convert to ArrayBuffer as requested by responseType
209+ this . response = allChunks . buffer ;
210+ } catch ( error ) {
211+ this . statusText = error . message ;
212+
213+ if ( error . name === 'AbortError' ) {
214+ // Do nothing.
215+ } else if ( error . name === 'TimeoutError' ) {
216+ this . ontimeout ?. ( ) ;
217+ } else {
218+ // This is a network error
219+ this . onerror ?. ( ) ;
220+ }
221+ } finally {
222+ clearTimeout ( timeoutID ) ;
223+ if ( ! this . _aborted ) {
224+ this . _changeReadyState ( 4 ) ; // 4: DONE
225+ // The XHR 'load' event fires for successful HTTP statuses (2xx) as well as
226+ // unsuccessful ones (4xx, 5xx). The 'error' event is for network failures.
227+ this . onload ?. ( ) ;
228+ }
229+ }
230+ }
231+
232+ /**
233+ * Aborts the request if it has already been sent.
234+ */
235+ abort ( ) {
236+ this . _aborted = true ;
237+ this . status = 0 ;
238+ this . _changeReadyState ( 4 ) ; // 4: DONE
239+ this . _abortController ?. abort ( ) ;
240+ }
241+ }
242+ #endif
243+
7244var Fetch = {
8245 // HandleAllocator for XHR request object
9246 // xhrs: undefined,
@@ -267,7 +504,18 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
267504 var userNameStr = userName ? UTF8ToString ( userName ) : undefined ;
268505 var passwordStr = password ? UTF8ToString ( password ) : undefined ;
269506
507+ #if FETCH_STREAMING == 1
508+ if ( fetchAttrStreamData ) {
509+ var xhr = new FetchXHR ( ) ;
510+ } else {
511+ var xhr = new XMLHttpRequest ( ) ;
512+ }
513+ #elif FETCH_STREAMING == 2
514+ // This setting forces using FetchXHR for all requests. Used only in testing.
515+ var xhr = new FetchXHR ( ) ;
516+ #else
270517 var xhr = new XMLHttpRequest ( ) ;
518+ #endif
271519 xhr . withCredentials = ! ! { { { makeGetValue ( 'fetch_attr' , C_STRUCTS . emscripten_fetch_attr_t . withCredentials , 'u8' ) } } } ; ;
272520#if FETCH_DEBUG
273521 dbg ( `fetch: xhr.timeout: ${ xhr . timeout } , xhr.withCredentials: ${ xhr . withCredentials } ` ) ;
@@ -276,8 +524,8 @@ function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
276524 xhr . open ( requestMethod , url_ , ! fetchAttrSynchronous , userNameStr , passwordStr ) ;
277525 if ( ! fetchAttrSynchronous ) xhr . timeout = timeoutMsecs ; // XHR timeout field is only accessible in async XHRs, and must be set after .open() but before .send().
278526 xhr . url_ = url_ ; // Save the url for debugging purposes (and for comparing to the responseURL that server side advertised)
279- #if ASSERTIONS
280- assert ( ! fetchAttrStreamData , 'streaming uses moz-chunked-arraybuffer which is no longer supported; TODO: rewrite using fetch() ' ) ;
527+ #if ASSERTIONS && ! FETCH_STREAMING
528+ assert ( ! fetchAttrStreamData , 'Streaming is only supported when FETCH_STREAMING is enabled. ' ) ;
281529#endif
282530 xhr . responseType = 'arraybuffer' ;
283531
0 commit comments