ajax-hooker 1.2.6 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/cjs/index.js +32 -1
- package/dist/esm/index.js +32 -1
- package/dist/iife/index.js +32 -1
- package/dist/types/fetch.d.ts +2 -0
- package/dist/types/type.d.ts +1 -0
- package/dist/types/xhr.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -295,6 +295,7 @@ The response object received by the response callback contains the following pro
|
|
|
295
295
|
| `arrayBuffer` | `ArrayBuffer` | Read-only | Fetch only. Response ArrayBuffer |
|
|
296
296
|
| `blob` | `Blob` | Read-only | Fetch only. Response Blob |
|
|
297
297
|
| `formData` | `FormData` | Read-only | Fetch only. Response FormData |
|
|
298
|
+
| `mockError` | `Error \| string` | **Writable** | Fetch only. Rejects the fetch promise after the real request has been sent |
|
|
298
299
|
|
|
299
300
|
> **Note:** For Fetch responses, `json`, `text`, `arrayBuffer`, `blob`, and `formData` are automatically parsed by the interceptor and available as properties. No need to call `.json()` or similar methods. If parsing fails, the corresponding property is `null`.
|
|
300
301
|
|
|
@@ -319,6 +320,7 @@ interface AjaxResponse {
|
|
|
319
320
|
arrayBuffer?: ArrayBuffer;
|
|
320
321
|
blob?: Blob;
|
|
321
322
|
formData?: FormData;
|
|
323
|
+
mockError?: Error | string;
|
|
322
324
|
}
|
|
323
325
|
```
|
|
324
326
|
|
|
@@ -406,6 +408,21 @@ interceptor.hook((request) => {
|
|
|
406
408
|
}, 'xhr');
|
|
407
409
|
```
|
|
408
410
|
|
|
411
|
+
### Simulate Fetch Network Errors
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
interceptor.hook((request) => {
|
|
415
|
+
if (request.url.includes('/api/fail')) {
|
|
416
|
+
request.response = (response) => {
|
|
417
|
+
response.mockError = new TypeError('Failed to fetch');
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return request;
|
|
421
|
+
}, 'fetch');
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
> `mockError` is designed for **Fetch only**. For XHR failure simulation, prefer setting `request.timeout` and using a slow or hanging endpoint to trigger native timeout behavior.
|
|
425
|
+
|
|
409
426
|
### Intercept Streaming Responses
|
|
410
427
|
|
|
411
428
|
```typescript
|
package/dist/cjs/index.js
CHANGED
|
@@ -52,7 +52,15 @@ class XhrInterceptor {
|
|
|
52
52
|
nativeXhr=window.XMLHttpRequest;
|
|
53
53
|
nativeXhrPrototype=window.XMLHttpRequest.prototype;
|
|
54
54
|
hooks=[];
|
|
55
|
+
xhrContructorKeys={
|
|
56
|
+
UNSENT: this.nativeXhr.UNSENT,
|
|
57
|
+
OPENED: this.nativeXhr.OPENED,
|
|
58
|
+
HEADERS_RECEIVED: this.nativeXhr.HEADERS_RECEIVED,
|
|
59
|
+
LOADING: this.nativeXhr.LOADING,
|
|
60
|
+
DONE: this.nativeXhr.DONE
|
|
61
|
+
};
|
|
55
62
|
xhrResponseEvents=[ "readystatechange", "load", "loadend" ];
|
|
63
|
+
xhrAssignedMethodOverrides=new WeakMap;
|
|
56
64
|
xhrInstanceAttr=[ "response", "responseText", "responseXML", "status", "statusText" ];
|
|
57
65
|
xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((acc, attr) => {
|
|
58
66
|
acc[attr] = function(target) {
|
|
@@ -97,7 +105,7 @@ class XhrInterceptor {
|
|
|
97
105
|
console.warn("[AjaxInterceptor] Error in xhr request hooks:", error);
|
|
98
106
|
}
|
|
99
107
|
hooker.req = newRequest;
|
|
100
|
-
if (target.readyState !==
|
|
108
|
+
if (target.readyState !== self.xhrContructorKeys.OPENED && target.readyState !== self.xhrContructorKeys.UNSENT) return;
|
|
101
109
|
const needReopen = oldRequest.method !== newRequest.method || oldRequest.url !== newRequest.url;
|
|
102
110
|
const headersChanged = !self.headersEqual(oldRequest.headers, newRequest.headers);
|
|
103
111
|
const shouldReopen = needReopen || headersChanged;
|
|
@@ -217,11 +225,24 @@ class XhrInterceptor {
|
|
|
217
225
|
};
|
|
218
226
|
return toSortedString(a) === toSortedString(b);
|
|
219
227
|
}
|
|
228
|
+
getAssignedMethodOverride(target, attr) {
|
|
229
|
+
return this.xhrAssignedMethodOverrides.get(target)?.[attr];
|
|
230
|
+
}
|
|
231
|
+
setAssignedMethodOverride(target, attr, value) {
|
|
232
|
+
let targetOverrides = this.xhrAssignedMethodOverrides.get(target);
|
|
233
|
+
if (!targetOverrides) {
|
|
234
|
+
targetOverrides = {};
|
|
235
|
+
this.xhrAssignedMethodOverrides.set(target, targetOverrides);
|
|
236
|
+
}
|
|
237
|
+
targetOverrides[attr] = value;
|
|
238
|
+
}
|
|
220
239
|
getCaptureOption(options) {
|
|
221
240
|
if (typeof options === "boolean") return options;
|
|
222
241
|
return !!options?.capture;
|
|
223
242
|
}
|
|
224
243
|
getAttrHandler(target, attr, receiver) {
|
|
244
|
+
const assignedMethodOverride = this.getAssignedMethodOverride(target, attr);
|
|
245
|
+
if (assignedMethodOverride !== void 0) return assignedMethodOverride;
|
|
225
246
|
if (this.xhrInstanceAttr.includes(attr)) return this.xhrInstanceAttrHandler[attr](target);
|
|
226
247
|
if (this.xhrMethodsHandler[attr]) return this.xhrMethodsHandler[attr](this, target, receiver);
|
|
227
248
|
return null;
|
|
@@ -236,6 +257,7 @@ class XhrInterceptor {
|
|
|
236
257
|
return self.getAttrHandler(target, prop, receiver) ?? getProxyValue(target, prop);
|
|
237
258
|
},
|
|
238
259
|
set(target, prop, value, receiver) {
|
|
260
|
+
if (self.xhrMethodsHandler[prop]) self.setAssignedMethodOverride(target, prop, value);
|
|
239
261
|
if (typeof value === "function" && prop.startsWith("on")) {
|
|
240
262
|
const isResponseEvent = self.xhrResponseEvents.includes(prop.replace(/^on/, ""));
|
|
241
263
|
const fn = async function(...args) {
|
|
@@ -406,6 +428,13 @@ class FetchInterceptor {
|
|
|
406
428
|
if (headers instanceof Headers) return headers;
|
|
407
429
|
return new Headers(headers);
|
|
408
430
|
}
|
|
431
|
+
resolveFetchError(error) {
|
|
432
|
+
if (error instanceof Error) return error;
|
|
433
|
+
return new TypeError(error);
|
|
434
|
+
}
|
|
435
|
+
throwIfMockError(mockError) {
|
|
436
|
+
if (mockError) throw this.resolveFetchError(mockError);
|
|
437
|
+
}
|
|
409
438
|
isWasmRequest(url) {
|
|
410
439
|
try {
|
|
411
440
|
return new URL(url).pathname.endsWith(".wasm");
|
|
@@ -470,6 +499,7 @@ class FetchInterceptor {
|
|
|
470
499
|
} catch (error) {
|
|
471
500
|
console.warn("[AjaxInterceptor] Error in fetch stream response callback:", error);
|
|
472
501
|
}
|
|
502
|
+
self.throwIfMockError(hooker.resp.mockError);
|
|
473
503
|
let chunkIndex = 0;
|
|
474
504
|
const {readable: readable, writable: writable} = new TransformStream({
|
|
475
505
|
async transform(chunk, controller) {
|
|
@@ -522,6 +552,7 @@ class FetchInterceptor {
|
|
|
522
552
|
} catch (error) {
|
|
523
553
|
console.warn("[AjaxInterceptor] Error in fetch response callback:", error);
|
|
524
554
|
}
|
|
555
|
+
self.throwIfMockError(hooker.resp.mockError);
|
|
525
556
|
}
|
|
526
557
|
interceptedResponse[CYCLE_SCHEDULER] = hooker;
|
|
527
558
|
const proxyFh = new Proxy(interceptedResponse, {
|
package/dist/esm/index.js
CHANGED
|
@@ -46,7 +46,15 @@ class XhrInterceptor {
|
|
|
46
46
|
nativeXhr=window.XMLHttpRequest;
|
|
47
47
|
nativeXhrPrototype=window.XMLHttpRequest.prototype;
|
|
48
48
|
hooks=[];
|
|
49
|
+
xhrContructorKeys={
|
|
50
|
+
UNSENT: this.nativeXhr.UNSENT,
|
|
51
|
+
OPENED: this.nativeXhr.OPENED,
|
|
52
|
+
HEADERS_RECEIVED: this.nativeXhr.HEADERS_RECEIVED,
|
|
53
|
+
LOADING: this.nativeXhr.LOADING,
|
|
54
|
+
DONE: this.nativeXhr.DONE
|
|
55
|
+
};
|
|
49
56
|
xhrResponseEvents=[ "readystatechange", "load", "loadend" ];
|
|
57
|
+
xhrAssignedMethodOverrides=new WeakMap;
|
|
50
58
|
xhrInstanceAttr=[ "response", "responseText", "responseXML", "status", "statusText" ];
|
|
51
59
|
xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((acc, attr) => {
|
|
52
60
|
acc[attr] = function(target) {
|
|
@@ -91,7 +99,7 @@ class XhrInterceptor {
|
|
|
91
99
|
console.warn("[AjaxInterceptor] Error in xhr request hooks:", error);
|
|
92
100
|
}
|
|
93
101
|
hooker.req = newRequest;
|
|
94
|
-
if (target.readyState !==
|
|
102
|
+
if (target.readyState !== self.xhrContructorKeys.OPENED && target.readyState !== self.xhrContructorKeys.UNSENT) return;
|
|
95
103
|
const needReopen = oldRequest.method !== newRequest.method || oldRequest.url !== newRequest.url;
|
|
96
104
|
const headersChanged = !self.headersEqual(oldRequest.headers, newRequest.headers);
|
|
97
105
|
const shouldReopen = needReopen || headersChanged;
|
|
@@ -211,11 +219,24 @@ class XhrInterceptor {
|
|
|
211
219
|
};
|
|
212
220
|
return toSortedString(a) === toSortedString(b);
|
|
213
221
|
}
|
|
222
|
+
getAssignedMethodOverride(target, attr) {
|
|
223
|
+
return this.xhrAssignedMethodOverrides.get(target)?.[attr];
|
|
224
|
+
}
|
|
225
|
+
setAssignedMethodOverride(target, attr, value) {
|
|
226
|
+
let targetOverrides = this.xhrAssignedMethodOverrides.get(target);
|
|
227
|
+
if (!targetOverrides) {
|
|
228
|
+
targetOverrides = {};
|
|
229
|
+
this.xhrAssignedMethodOverrides.set(target, targetOverrides);
|
|
230
|
+
}
|
|
231
|
+
targetOverrides[attr] = value;
|
|
232
|
+
}
|
|
214
233
|
getCaptureOption(options) {
|
|
215
234
|
if (typeof options === "boolean") return options;
|
|
216
235
|
return !!options?.capture;
|
|
217
236
|
}
|
|
218
237
|
getAttrHandler(target, attr, receiver) {
|
|
238
|
+
const assignedMethodOverride = this.getAssignedMethodOverride(target, attr);
|
|
239
|
+
if (assignedMethodOverride !== void 0) return assignedMethodOverride;
|
|
219
240
|
if (this.xhrInstanceAttr.includes(attr)) return this.xhrInstanceAttrHandler[attr](target);
|
|
220
241
|
if (this.xhrMethodsHandler[attr]) return this.xhrMethodsHandler[attr](this, target, receiver);
|
|
221
242
|
return null;
|
|
@@ -230,6 +251,7 @@ class XhrInterceptor {
|
|
|
230
251
|
return self.getAttrHandler(target, prop, receiver) ?? getProxyValue(target, prop);
|
|
231
252
|
},
|
|
232
253
|
set(target, prop, value, receiver) {
|
|
254
|
+
if (self.xhrMethodsHandler[prop]) self.setAssignedMethodOverride(target, prop, value);
|
|
233
255
|
if (typeof value === "function" && prop.startsWith("on")) {
|
|
234
256
|
const isResponseEvent = self.xhrResponseEvents.includes(prop.replace(/^on/, ""));
|
|
235
257
|
const fn = async function(...args) {
|
|
@@ -400,6 +422,13 @@ class FetchInterceptor {
|
|
|
400
422
|
if (headers instanceof Headers) return headers;
|
|
401
423
|
return new Headers(headers);
|
|
402
424
|
}
|
|
425
|
+
resolveFetchError(error) {
|
|
426
|
+
if (error instanceof Error) return error;
|
|
427
|
+
return new TypeError(error);
|
|
428
|
+
}
|
|
429
|
+
throwIfMockError(mockError) {
|
|
430
|
+
if (mockError) throw this.resolveFetchError(mockError);
|
|
431
|
+
}
|
|
403
432
|
isWasmRequest(url) {
|
|
404
433
|
try {
|
|
405
434
|
return new URL(url).pathname.endsWith(".wasm");
|
|
@@ -464,6 +493,7 @@ class FetchInterceptor {
|
|
|
464
493
|
} catch (error) {
|
|
465
494
|
console.warn("[AjaxInterceptor] Error in fetch stream response callback:", error);
|
|
466
495
|
}
|
|
496
|
+
self.throwIfMockError(hooker.resp.mockError);
|
|
467
497
|
let chunkIndex = 0;
|
|
468
498
|
const {readable: readable, writable: writable} = new TransformStream({
|
|
469
499
|
async transform(chunk, controller) {
|
|
@@ -516,6 +546,7 @@ class FetchInterceptor {
|
|
|
516
546
|
} catch (error) {
|
|
517
547
|
console.warn("[AjaxInterceptor] Error in fetch response callback:", error);
|
|
518
548
|
}
|
|
549
|
+
self.throwIfMockError(hooker.resp.mockError);
|
|
519
550
|
}
|
|
520
551
|
interceptedResponse[CYCLE_SCHEDULER] = hooker;
|
|
521
552
|
const proxyFh = new Proxy(interceptedResponse, {
|
package/dist/iife/index.js
CHANGED
|
@@ -40,7 +40,15 @@ var AjaxHooker = function(exports) {
|
|
|
40
40
|
nativeXhr=window.XMLHttpRequest;
|
|
41
41
|
nativeXhrPrototype=window.XMLHttpRequest.prototype;
|
|
42
42
|
hooks=[];
|
|
43
|
+
xhrContructorKeys={
|
|
44
|
+
UNSENT: this.nativeXhr.UNSENT,
|
|
45
|
+
OPENED: this.nativeXhr.OPENED,
|
|
46
|
+
HEADERS_RECEIVED: this.nativeXhr.HEADERS_RECEIVED,
|
|
47
|
+
LOADING: this.nativeXhr.LOADING,
|
|
48
|
+
DONE: this.nativeXhr.DONE
|
|
49
|
+
};
|
|
43
50
|
xhrResponseEvents=[ "readystatechange", "load", "loadend" ];
|
|
51
|
+
xhrAssignedMethodOverrides=new WeakMap;
|
|
44
52
|
xhrInstanceAttr=[ "response", "responseText", "responseXML", "status", "statusText" ];
|
|
45
53
|
xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((acc, attr) => {
|
|
46
54
|
acc[attr] = function(target) {
|
|
@@ -85,7 +93,7 @@ var AjaxHooker = function(exports) {
|
|
|
85
93
|
console.warn("[AjaxInterceptor] Error in xhr request hooks:", error);
|
|
86
94
|
}
|
|
87
95
|
hooker.req = newRequest;
|
|
88
|
-
if (target.readyState !==
|
|
96
|
+
if (target.readyState !== self.xhrContructorKeys.OPENED && target.readyState !== self.xhrContructorKeys.UNSENT) return;
|
|
89
97
|
const needReopen = oldRequest.method !== newRequest.method || oldRequest.url !== newRequest.url;
|
|
90
98
|
const headersChanged = !self.headersEqual(oldRequest.headers, newRequest.headers);
|
|
91
99
|
const shouldReopen = needReopen || headersChanged;
|
|
@@ -205,11 +213,24 @@ var AjaxHooker = function(exports) {
|
|
|
205
213
|
};
|
|
206
214
|
return toSortedString(a) === toSortedString(b);
|
|
207
215
|
}
|
|
216
|
+
getAssignedMethodOverride(target, attr) {
|
|
217
|
+
return this.xhrAssignedMethodOverrides.get(target)?.[attr];
|
|
218
|
+
}
|
|
219
|
+
setAssignedMethodOverride(target, attr, value) {
|
|
220
|
+
let targetOverrides = this.xhrAssignedMethodOverrides.get(target);
|
|
221
|
+
if (!targetOverrides) {
|
|
222
|
+
targetOverrides = {};
|
|
223
|
+
this.xhrAssignedMethodOverrides.set(target, targetOverrides);
|
|
224
|
+
}
|
|
225
|
+
targetOverrides[attr] = value;
|
|
226
|
+
}
|
|
208
227
|
getCaptureOption(options) {
|
|
209
228
|
if (typeof options === "boolean") return options;
|
|
210
229
|
return !!options?.capture;
|
|
211
230
|
}
|
|
212
231
|
getAttrHandler(target, attr, receiver) {
|
|
232
|
+
const assignedMethodOverride = this.getAssignedMethodOverride(target, attr);
|
|
233
|
+
if (assignedMethodOverride !== void 0) return assignedMethodOverride;
|
|
213
234
|
if (this.xhrInstanceAttr.includes(attr)) return this.xhrInstanceAttrHandler[attr](target);
|
|
214
235
|
if (this.xhrMethodsHandler[attr]) return this.xhrMethodsHandler[attr](this, target, receiver);
|
|
215
236
|
return null;
|
|
@@ -224,6 +245,7 @@ var AjaxHooker = function(exports) {
|
|
|
224
245
|
return self.getAttrHandler(target, prop, receiver) ?? getProxyValue(target, prop);
|
|
225
246
|
},
|
|
226
247
|
set(target, prop, value, receiver) {
|
|
248
|
+
if (self.xhrMethodsHandler[prop]) self.setAssignedMethodOverride(target, prop, value);
|
|
227
249
|
if (typeof value === "function" && prop.startsWith("on")) {
|
|
228
250
|
const isResponseEvent = self.xhrResponseEvents.includes(prop.replace(/^on/, ""));
|
|
229
251
|
const fn = async function(...args) {
|
|
@@ -391,6 +413,13 @@ var AjaxHooker = function(exports) {
|
|
|
391
413
|
if (headers instanceof Headers) return headers;
|
|
392
414
|
return new Headers(headers);
|
|
393
415
|
}
|
|
416
|
+
resolveFetchError(error) {
|
|
417
|
+
if (error instanceof Error) return error;
|
|
418
|
+
return new TypeError(error);
|
|
419
|
+
}
|
|
420
|
+
throwIfMockError(mockError) {
|
|
421
|
+
if (mockError) throw this.resolveFetchError(mockError);
|
|
422
|
+
}
|
|
394
423
|
isWasmRequest(url) {
|
|
395
424
|
try {
|
|
396
425
|
return new URL(url).pathname.endsWith(".wasm");
|
|
@@ -455,6 +484,7 @@ var AjaxHooker = function(exports) {
|
|
|
455
484
|
} catch (error) {
|
|
456
485
|
console.warn("[AjaxInterceptor] Error in fetch stream response callback:", error);
|
|
457
486
|
}
|
|
487
|
+
self.throwIfMockError(hooker.resp.mockError);
|
|
458
488
|
let chunkIndex = 0;
|
|
459
489
|
const {readable: readable, writable: writable} = new TransformStream({
|
|
460
490
|
async transform(chunk, controller) {
|
|
@@ -507,6 +537,7 @@ var AjaxHooker = function(exports) {
|
|
|
507
537
|
} catch (error) {
|
|
508
538
|
console.warn("[AjaxInterceptor] Error in fetch response callback:", error);
|
|
509
539
|
}
|
|
540
|
+
self.throwIfMockError(hooker.resp.mockError);
|
|
510
541
|
}
|
|
511
542
|
interceptedResponse[CYCLE_SCHEDULER] = hooker;
|
|
512
543
|
const proxyFh = new Proxy(interceptedResponse, {
|
package/dist/types/fetch.d.ts
CHANGED
package/dist/types/type.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface FetchResponse extends Pick<Response, 'ok' | 'redirected'> {
|
|
|
13
13
|
blob: Blob;
|
|
14
14
|
formData: FormData;
|
|
15
15
|
json: any;
|
|
16
|
+
mockError?: Error | string;
|
|
16
17
|
}
|
|
17
18
|
export interface AjaxResponse extends BaseResponse, Partial<XhrResponse & FetchResponse> {
|
|
18
19
|
}
|
package/dist/types/xhr.d.ts
CHANGED
|
@@ -11,7 +11,9 @@ export declare class XhrInterceptor {
|
|
|
11
11
|
};
|
|
12
12
|
readonly nativeXhrPrototype: XMLHttpRequest;
|
|
13
13
|
hooks: HookFunction[];
|
|
14
|
+
private readonly xhrContructorKeys;
|
|
14
15
|
private xhrResponseEvents;
|
|
16
|
+
private xhrAssignedMethodOverrides;
|
|
15
17
|
private xhrInstanceAttr;
|
|
16
18
|
private xhrInstanceAttrHandler;
|
|
17
19
|
private xhrMethodsHandler;
|
|
@@ -21,6 +23,8 @@ export declare class XhrInterceptor {
|
|
|
21
23
|
private parseHeaders;
|
|
22
24
|
private responseProcessor;
|
|
23
25
|
private headersEqual;
|
|
26
|
+
private getAssignedMethodOverride;
|
|
27
|
+
private setAssignedMethodOverride;
|
|
24
28
|
private getCaptureOption;
|
|
25
29
|
private getAttrHandler;
|
|
26
30
|
private _generateProxyXMLHttpRequest;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ajax-hooker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Browser AJAX interceptor for XMLHttpRequest and fetch with unified hooks, request/response mutation, and streaming response support.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|