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 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 !== XMLHttpRequest.OPENED) return;
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 !== XMLHttpRequest.OPENED) return;
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, {
@@ -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 !== XMLHttpRequest.OPENED) return;
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, {
@@ -15,6 +15,8 @@ export declare class FetchInterceptor {
15
15
  private resolveRequest;
16
16
  private resolveOptions;
17
17
  private resolveHeaders;
18
+ private resolveFetchError;
19
+ private throwIfMockError;
18
20
  private isWasmRequest;
19
21
  private _generateProxyFetch;
20
22
  }
@@ -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
  }
@@ -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.2.6",
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",