ajax-hooker 1.2.5 → 1.3.0

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
@@ -31,7 +31,7 @@ Object.getOwnPropertyDescriptor.bind(Object);
31
31
 
32
32
  const getType = Object.prototype.toString.call.bind(Object.prototype.toString);
33
33
 
34
- const resolveUrl = (url = "") => new URL(url, window.location.origin).toString();
34
+ const resolveUrl = (url = "") => new URL(url, window.location.href).toString();
35
35
 
36
36
  const getProxyValue = (target, prop) => {
37
37
  const value = Reflect.get(target, prop);
@@ -52,6 +52,13 @@ 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" ];
56
63
  xhrInstanceAttr=[ "response", "responseText", "responseXML", "status", "statusText" ];
57
64
  xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((acc, attr) => {
@@ -97,6 +104,7 @@ class XhrInterceptor {
97
104
  console.warn("[AjaxInterceptor] Error in xhr request hooks:", error);
98
105
  }
99
106
  hooker.req = newRequest;
107
+ if (target.readyState !== self.xhrContructorKeys.OPENED && target.readyState !== self.xhrContructorKeys.UNSENT) return;
100
108
  const needReopen = oldRequest.method !== newRequest.method || oldRequest.url !== newRequest.url;
101
109
  const headersChanged = !self.headersEqual(oldRequest.headers, newRequest.headers);
102
110
  const shouldReopen = needReopen || headersChanged;
@@ -354,7 +362,8 @@ class FetchInterceptor {
354
362
  };
355
363
  }
356
364
  resolveRequest(req, newRequest, sourceMap) {
357
- const urlChanged = typeof req === "string" && newRequest.url !== req || req instanceof URL && newRequest.url !== req.href || req instanceof Request && newRequest.url !== req.url;
365
+ const resolvedOriginalUrl = typeof req === "string" ? resolveUrl(req) : req instanceof URL ? req.href : req.url;
366
+ const urlChanged = newRequest.url !== resolvedOriginalUrl;
358
367
  if (typeof req === "string") return urlChanged ? newRequest.url : req;
359
368
  if (req instanceof URL) return urlChanged ? new URL(newRequest.url) : req;
360
369
  if (req instanceof Request) {
@@ -404,6 +413,13 @@ class FetchInterceptor {
404
413
  if (headers instanceof Headers) return headers;
405
414
  return new Headers(headers);
406
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
+ }
407
423
  isWasmRequest(url) {
408
424
  try {
409
425
  return new URL(url).pathname.endsWith(".wasm");
@@ -468,6 +484,7 @@ class FetchInterceptor {
468
484
  } catch (error) {
469
485
  console.warn("[AjaxInterceptor] Error in fetch stream response callback:", error);
470
486
  }
487
+ self.throwIfMockError(hooker.resp.mockError);
471
488
  let chunkIndex = 0;
472
489
  const {readable: readable, writable: writable} = new TransformStream({
473
490
  async transform(chunk, controller) {
@@ -520,6 +537,7 @@ class FetchInterceptor {
520
537
  } catch (error) {
521
538
  console.warn("[AjaxInterceptor] Error in fetch response callback:", error);
522
539
  }
540
+ self.throwIfMockError(hooker.resp.mockError);
523
541
  }
524
542
  interceptedResponse[CYCLE_SCHEDULER] = hooker;
525
543
  const proxyFh = new Proxy(interceptedResponse, {
package/dist/esm/index.js CHANGED
@@ -25,7 +25,7 @@ Object.getOwnPropertyDescriptor.bind(Object);
25
25
 
26
26
  const getType = Object.prototype.toString.call.bind(Object.prototype.toString);
27
27
 
28
- const resolveUrl = (url = "") => new URL(url, window.location.origin).toString();
28
+ const resolveUrl = (url = "") => new URL(url, window.location.href).toString();
29
29
 
30
30
  const getProxyValue = (target, prop) => {
31
31
  const value = Reflect.get(target, prop);
@@ -46,6 +46,13 @@ 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" ];
50
57
  xhrInstanceAttr=[ "response", "responseText", "responseXML", "status", "statusText" ];
51
58
  xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((acc, attr) => {
@@ -91,6 +98,7 @@ class XhrInterceptor {
91
98
  console.warn("[AjaxInterceptor] Error in xhr request hooks:", error);
92
99
  }
93
100
  hooker.req = newRequest;
101
+ if (target.readyState !== self.xhrContructorKeys.OPENED && target.readyState !== self.xhrContructorKeys.UNSENT) return;
94
102
  const needReopen = oldRequest.method !== newRequest.method || oldRequest.url !== newRequest.url;
95
103
  const headersChanged = !self.headersEqual(oldRequest.headers, newRequest.headers);
96
104
  const shouldReopen = needReopen || headersChanged;
@@ -348,7 +356,8 @@ class FetchInterceptor {
348
356
  };
349
357
  }
350
358
  resolveRequest(req, newRequest, sourceMap) {
351
- const urlChanged = typeof req === "string" && newRequest.url !== req || req instanceof URL && newRequest.url !== req.href || req instanceof Request && newRequest.url !== req.url;
359
+ const resolvedOriginalUrl = typeof req === "string" ? resolveUrl(req) : req instanceof URL ? req.href : req.url;
360
+ const urlChanged = newRequest.url !== resolvedOriginalUrl;
352
361
  if (typeof req === "string") return urlChanged ? newRequest.url : req;
353
362
  if (req instanceof URL) return urlChanged ? new URL(newRequest.url) : req;
354
363
  if (req instanceof Request) {
@@ -398,6 +407,13 @@ class FetchInterceptor {
398
407
  if (headers instanceof Headers) return headers;
399
408
  return new Headers(headers);
400
409
  }
410
+ resolveFetchError(error) {
411
+ if (error instanceof Error) return error;
412
+ return new TypeError(error);
413
+ }
414
+ throwIfMockError(mockError) {
415
+ if (mockError) throw this.resolveFetchError(mockError);
416
+ }
401
417
  isWasmRequest(url) {
402
418
  try {
403
419
  return new URL(url).pathname.endsWith(".wasm");
@@ -462,6 +478,7 @@ class FetchInterceptor {
462
478
  } catch (error) {
463
479
  console.warn("[AjaxInterceptor] Error in fetch stream response callback:", error);
464
480
  }
481
+ self.throwIfMockError(hooker.resp.mockError);
465
482
  let chunkIndex = 0;
466
483
  const {readable: readable, writable: writable} = new TransformStream({
467
484
  async transform(chunk, controller) {
@@ -514,6 +531,7 @@ class FetchInterceptor {
514
531
  } catch (error) {
515
532
  console.warn("[AjaxInterceptor] Error in fetch response callback:", error);
516
533
  }
534
+ self.throwIfMockError(hooker.resp.mockError);
517
535
  }
518
536
  interceptedResponse[CYCLE_SCHEDULER] = hooker;
519
537
  const proxyFh = new Proxy(interceptedResponse, {
@@ -22,7 +22,7 @@ var AjaxHooker = function(exports) {
22
22
  }
23
23
  Object.getOwnPropertyDescriptor.bind(Object);
24
24
  const getType = Object.prototype.toString.call.bind(Object.prototype.toString);
25
- const resolveUrl = (url = "") => new URL(url, window.location.origin).toString();
25
+ const resolveUrl = (url = "") => new URL(url, window.location.href).toString();
26
26
  const getProxyValue = (target, prop) => {
27
27
  const value = Reflect.get(target, prop);
28
28
  if (typeof value !== "function") return value;
@@ -40,6 +40,13 @@ 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" ];
44
51
  xhrInstanceAttr=[ "response", "responseText", "responseXML", "status", "statusText" ];
45
52
  xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((acc, attr) => {
@@ -85,6 +92,7 @@ var AjaxHooker = function(exports) {
85
92
  console.warn("[AjaxInterceptor] Error in xhr request hooks:", error);
86
93
  }
87
94
  hooker.req = newRequest;
95
+ if (target.readyState !== self.xhrContructorKeys.OPENED && target.readyState !== self.xhrContructorKeys.UNSENT) return;
88
96
  const needReopen = oldRequest.method !== newRequest.method || oldRequest.url !== newRequest.url;
89
97
  const headersChanged = !self.headersEqual(oldRequest.headers, newRequest.headers);
90
98
  const shouldReopen = needReopen || headersChanged;
@@ -339,7 +347,8 @@ var AjaxHooker = function(exports) {
339
347
  };
340
348
  }
341
349
  resolveRequest(req, newRequest, sourceMap) {
342
- const urlChanged = typeof req === "string" && newRequest.url !== req || req instanceof URL && newRequest.url !== req.href || req instanceof Request && newRequest.url !== req.url;
350
+ const resolvedOriginalUrl = typeof req === "string" ? resolveUrl(req) : req instanceof URL ? req.href : req.url;
351
+ const urlChanged = newRequest.url !== resolvedOriginalUrl;
343
352
  if (typeof req === "string") return urlChanged ? newRequest.url : req;
344
353
  if (req instanceof URL) return urlChanged ? new URL(newRequest.url) : req;
345
354
  if (req instanceof Request) {
@@ -389,6 +398,13 @@ var AjaxHooker = function(exports) {
389
398
  if (headers instanceof Headers) return headers;
390
399
  return new Headers(headers);
391
400
  }
401
+ resolveFetchError(error) {
402
+ if (error instanceof Error) return error;
403
+ return new TypeError(error);
404
+ }
405
+ throwIfMockError(mockError) {
406
+ if (mockError) throw this.resolveFetchError(mockError);
407
+ }
392
408
  isWasmRequest(url) {
393
409
  try {
394
410
  return new URL(url).pathname.endsWith(".wasm");
@@ -453,6 +469,7 @@ var AjaxHooker = function(exports) {
453
469
  } catch (error) {
454
470
  console.warn("[AjaxInterceptor] Error in fetch stream response callback:", error);
455
471
  }
472
+ self.throwIfMockError(hooker.resp.mockError);
456
473
  let chunkIndex = 0;
457
474
  const {readable: readable, writable: writable} = new TransformStream({
458
475
  async transform(chunk, controller) {
@@ -505,6 +522,7 @@ var AjaxHooker = function(exports) {
505
522
  } catch (error) {
506
523
  console.warn("[AjaxInterceptor] Error in fetch response callback:", error);
507
524
  }
525
+ self.throwIfMockError(hooker.resp.mockError);
508
526
  }
509
527
  interceptedResponse[CYCLE_SCHEDULER] = hooker;
510
528
  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,6 +11,7 @@ export declare class XhrInterceptor {
11
11
  };
12
12
  readonly nativeXhrPrototype: XMLHttpRequest;
13
13
  hooks: HookFunction[];
14
+ private readonly xhrContructorKeys;
14
15
  private xhrResponseEvents;
15
16
  private xhrInstanceAttr;
16
17
  private xhrInstanceAttrHandler;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ajax-hooker",
3
- "version": "1.2.5",
3
+ "version": "1.3.0",
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",