3
3
objectToString ,
4
4
generateRandomResponse ,
5
5
matchRequestProps ,
6
+ getXhrData ,
6
7
logMessage ,
7
8
// following helpers should be imported and injected
8
9
// because they are used by helpers above
@@ -95,16 +96,18 @@ export function preventXHR(source, propsToMatch, customResponseText) {
95
96
return ;
96
97
}
97
98
98
- let response = '' ;
99
- let responseText = '' ;
100
- let responseUrl ;
99
+ const nativeOpen = window . XMLHttpRequest . prototype . open ;
100
+ const nativeSend = window . XMLHttpRequest . prototype . send ;
101
+
102
+ let xhrData ;
103
+ let modifiedResponse = '' ;
104
+ let modifiedResponseText = '' ;
105
+
101
106
const openWrapper = ( target , thisArg , args ) => {
102
- // Get method and url from .open()
103
- const xhrData = {
104
- method : args [ 0 ] ,
105
- url : args [ 1 ] ,
106
- } ;
107
- responseUrl = xhrData . url ;
107
+ // Get original request properties
108
+ // eslint-disable-next-line prefer-spread
109
+ xhrData = getXhrData . apply ( null , args ) ;
110
+
108
111
if ( typeof propsToMatch === 'undefined' ) {
109
112
// Log if no propsToMatch given
110
113
logMessage ( source , `xhr( ${ objectToString ( xhrData ) } )` , true ) ;
@@ -113,6 +116,22 @@ export function preventXHR(source, propsToMatch, customResponseText) {
113
116
thisArg . shouldBePrevented = true ;
114
117
}
115
118
119
+ // Trap setRequestHeader of target xhr object to mimic request headers later;
120
+ // needed for getResponseHeader() and getAllResponseHeaders() methods
121
+ if ( thisArg . shouldBePrevented ) {
122
+ thisArg . collectedHeaders = [ ] ;
123
+ const setRequestHeaderWrapper = ( target , thisArg , args ) => {
124
+ // Collect headers
125
+ thisArg . collectedHeaders . push ( args ) ;
126
+ return Reflect . apply ( target , thisArg , args ) ;
127
+ } ;
128
+ const setRequestHeaderHandler = {
129
+ apply : setRequestHeaderWrapper ,
130
+ } ;
131
+ // setRequestHeader() can only be called on xhr.open(),
132
+ // so we can safely proxy it here
133
+ thisArg . setRequestHeader = new Proxy ( thisArg . setRequestHeader , setRequestHeaderHandler ) ;
134
+ }
116
135
return Reflect . apply ( target , thisArg , args ) ;
117
136
} ;
118
137
@@ -122,57 +141,143 @@ export function preventXHR(source, propsToMatch, customResponseText) {
122
141
}
123
142
124
143
if ( thisArg . responseType === 'blob' ) {
125
- response = new Blob ( ) ;
144
+ modifiedResponse = new Blob ( ) ;
126
145
}
127
-
128
146
if ( thisArg . responseType === 'arraybuffer' ) {
129
- response = new ArrayBuffer ( ) ;
147
+ modifiedResponse = new ArrayBuffer ( ) ;
130
148
}
131
149
132
150
if ( customResponseText ) {
133
151
const randomText = generateRandomResponse ( customResponseText ) ;
134
152
if ( randomText ) {
135
- responseText = randomText ;
153
+ modifiedResponseText = randomText ;
136
154
} else {
155
+ // FIXME: improve error text
137
156
logMessage ( source , `Invalid range: ${ customResponseText } ` ) ;
138
157
}
139
158
}
140
- // Mock response object
141
- Object . defineProperties ( thisArg , {
142
- readyState : { value : 4 , writable : false } ,
143
- response : { value : response , writable : false } ,
144
- responseText : { value : responseText , writable : false } ,
145
- responseURL : { value : responseUrl , writable : false } ,
146
- responseXML : { value : '' , writable : false } ,
147
- status : { value : 200 , writable : false } ,
148
- statusText : { value : 'OK' , writable : false } ,
159
+
160
+ /**
161
+ * Create separate XHR request with original request's input
162
+ * to be able to collect response data without triggering
163
+ * listeners on original XHR object
164
+ */
165
+ const forgedRequest = new XMLHttpRequest ( ) ;
166
+ forgedRequest . addEventListener ( 'readystatechange' , ( ) => {
167
+ if ( forgedRequest . readyState !== 4 ) {
168
+ return ;
169
+ }
170
+
171
+ const {
172
+ readyState,
173
+ responseURL,
174
+ responseXML,
175
+ status,
176
+ statusText,
177
+ } = forgedRequest ;
178
+
179
+ // Mock response object
180
+ Object . defineProperties ( thisArg , {
181
+ // original values
182
+ readyState : { value : readyState , writable : false } ,
183
+ status : { value : status , writable : false } ,
184
+ statusText : { value : statusText , writable : false } ,
185
+ responseURL : { value : responseURL , writable : false } ,
186
+ responseXML : { value : responseXML , writable : false } ,
187
+ // modified values
188
+ response : { value : modifiedResponse , writable : false } ,
189
+ responseText : { value : modifiedResponseText , writable : false } ,
190
+ } ) ;
191
+
192
+ // Mock events
193
+ setTimeout ( ( ) => {
194
+ const stateEvent = new Event ( 'readystatechange' ) ;
195
+ thisArg . dispatchEvent ( stateEvent ) ;
196
+
197
+ const loadEvent = new Event ( 'load' ) ;
198
+ thisArg . dispatchEvent ( loadEvent ) ;
199
+
200
+ const loadEndEvent = new Event ( 'loadend' ) ;
201
+ thisArg . dispatchEvent ( loadEndEvent ) ;
202
+ } , 1 ) ;
203
+
204
+ hit ( source ) ;
149
205
} ) ;
150
- // Mock events
151
- setTimeout ( ( ) => {
152
- const stateEvent = new Event ( 'readystatechange' ) ;
153
- thisArg . dispatchEvent ( stateEvent ) ;
154
206
155
- const loadEvent = new Event ( 'load' ) ;
156
- thisArg . dispatchEvent ( loadEvent ) ;
207
+ nativeOpen . apply ( forgedRequest , [ xhrData . method , xhrData . url ] ) ;
208
+
209
+ // Mimic request headers before sending
210
+ // setRequestHeader can only be called on open request objects
211
+ thisArg . collectedHeaders . forEach ( ( header ) => {
212
+ const name = header [ 0 ] ;
213
+ const value = header [ 1 ] ;
214
+ forgedRequest . setRequestHeader ( name , value ) ;
215
+ } ) ;
157
216
158
- const loadEndEvent = new Event ( 'loadend' ) ;
159
- thisArg . dispatchEvent ( loadEndEvent ) ;
160
- } , 1 ) ;
217
+ try {
218
+ nativeSend . call ( forgedRequest , args ) ;
219
+ } catch {
220
+ return Reflect . apply ( target , thisArg , args ) ;
221
+ }
161
222
162
- hit ( source ) ;
163
223
return undefined ;
164
224
} ;
165
225
226
+ const getHeaderWrapper = ( target , thisArg , args ) => {
227
+ if ( ! thisArg . collectedHeaders . length ) {
228
+ return null ;
229
+ }
230
+ // The search for the header name is case-insensitive
231
+ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getResponseHeader
232
+ const searchHeaderName = args [ 0 ] . toLowerCase ( ) ;
233
+ const matchedHeader = thisArg . collectedHeaders . find ( ( header ) => {
234
+ const headerName = header [ 0 ] . toLowerCase ( ) ;
235
+ return headerName === searchHeaderName ;
236
+ } ) ;
237
+ return matchedHeader
238
+ ? matchedHeader [ 1 ]
239
+ : null ;
240
+ } ;
241
+
242
+ const getAllHeadersWrapper = ( target , thisArg ) => {
243
+ if ( ! thisArg . collectedHeaders . length ) {
244
+ return '' ;
245
+ }
246
+ const allHeadersStr = thisArg . collectedHeaders
247
+ . map ( ( header ) => {
248
+ const headerName = header [ 0 ] ;
249
+ const headerValue = header [ 1 ] ;
250
+ // In modern browsers, the header names are returned in all lower case, as per the latest spec.
251
+ // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders
252
+ return `${ headerName . toLowerCase ( ) } : ${ headerValue } ` ;
253
+ } )
254
+ . join ( '\r\n' ) ;
255
+ return allHeadersStr ;
256
+ } ;
257
+
166
258
const openHandler = {
167
259
apply : openWrapper ,
168
260
} ;
169
-
170
261
const sendHandler = {
171
262
apply : sendWrapper ,
172
263
} ;
264
+ const getHeaderHandler = {
265
+ apply : getHeaderWrapper ,
266
+ } ;
267
+ const getAllHeadersHandler = {
268
+ apply : getAllHeadersWrapper ,
269
+ } ;
173
270
174
271
XMLHttpRequest . prototype . open = new Proxy ( XMLHttpRequest . prototype . open , openHandler ) ;
175
272
XMLHttpRequest . prototype . send = new Proxy ( XMLHttpRequest . prototype . send , sendHandler ) ;
273
+ XMLHttpRequest . prototype . getResponseHeader = new Proxy (
274
+ XMLHttpRequest . prototype . getResponseHeader ,
275
+ getHeaderHandler ,
276
+ ) ;
277
+ XMLHttpRequest . prototype . getAllResponseHeaders = new Proxy (
278
+ XMLHttpRequest . prototype . getAllResponseHeaders ,
279
+ getAllHeadersHandler ,
280
+ ) ;
176
281
}
177
282
178
283
preventXHR . names = [
@@ -185,10 +290,11 @@ preventXHR.names = [
185
290
186
291
preventXHR . injections = [
187
292
hit ,
188
- logMessage ,
189
293
objectToString ,
190
- matchRequestProps ,
191
294
generateRandomResponse ,
295
+ matchRequestProps ,
296
+ getXhrData ,
297
+ logMessage ,
192
298
toRegExp ,
193
299
isValidStrPattern ,
194
300
escapeRegExp ,
0 commit comments