@schmock/core 2.2.3 → 2.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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -0
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +30 -0
  6. package/dist/binary.d.ts +8 -0
  7. package/dist/binary.d.ts.map +1 -0
  8. package/dist/binary.js +9 -0
  9. package/dist/builder.d.ts +31 -10
  10. package/dist/builder.d.ts.map +1 -1
  11. package/dist/builder.js +804 -203
  12. package/dist/constants.d.ts +38 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +123 -2
  15. package/dist/errors.d.ts +14 -3
  16. package/dist/errors.d.ts.map +1 -1
  17. package/dist/errors.js +41 -6
  18. package/dist/helpers.d.ts +9 -0
  19. package/dist/helpers.d.ts.map +1 -1
  20. package/dist/helpers.js +18 -2
  21. package/dist/http-helpers.d.ts +52 -5
  22. package/dist/http-helpers.d.ts.map +1 -1
  23. package/dist/http-helpers.js +153 -37
  24. package/dist/index.d.ts +381 -60
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +9 -3
  27. package/dist/interceptor.d.ts +11 -1
  28. package/dist/interceptor.d.ts.map +1 -1
  29. package/dist/interceptor.js +354 -156
  30. package/dist/parser.d.ts.map +1 -1
  31. package/dist/parser.js +12 -3
  32. package/dist/plugin-pipeline.d.ts +11 -4
  33. package/dist/plugin-pipeline.d.ts.map +1 -1
  34. package/dist/plugin-pipeline.js +117 -39
  35. package/dist/response-normalizer.d.ts +16 -0
  36. package/dist/response-normalizer.d.ts.map +1 -0
  37. package/dist/response-normalizer.js +316 -0
  38. package/dist/response-parser.d.ts.map +1 -1
  39. package/dist/response-parser.js +85 -18
  40. package/dist/route-matcher.d.ts +3 -0
  41. package/dist/route-matcher.d.ts.map +1 -1
  42. package/dist/route-matcher.js +12 -6
  43. package/dist/types.d.ts +7 -2
  44. package/dist/types.d.ts.map +1 -1
  45. package/package.json +18 -8
  46. package/src/audit-core-builder.test.ts +0 -218
  47. package/src/audit-response-guard.test.ts +0 -38
  48. package/src/builder.test.ts +0 -289
  49. package/src/builder.ts +0 -701
  50. package/src/constants.test.ts +0 -99
  51. package/src/constants.ts +0 -73
  52. package/src/debug.test.ts +0 -241
  53. package/src/delay.test.ts +0 -319
  54. package/src/dist-shape.test.ts +0 -35
  55. package/src/errors.test.ts +0 -223
  56. package/src/errors.ts +0 -130
  57. package/src/factory.test.ts +0 -133
  58. package/src/helpers.test.ts +0 -147
  59. package/src/helpers.ts +0 -58
  60. package/src/http-helpers.ts +0 -113
  61. package/src/index.ts +0 -151
  62. package/src/interceptor.test.ts +0 -291
  63. package/src/interceptor.ts +0 -320
  64. package/src/namespace.test.ts +0 -274
  65. package/src/parser.property.test.ts +0 -594
  66. package/src/parser.test.ts +0 -148
  67. package/src/parser.ts +0 -64
  68. package/src/plugin-pipeline.ts +0 -103
  69. package/src/plugin-system.test.ts +0 -602
  70. package/src/response-parser.ts +0 -75
  71. package/src/response-parsing.test.ts +0 -333
  72. package/src/route-matcher.ts +0 -69
  73. package/src/route-matching.test.ts +0 -394
  74. package/src/server.test.ts +0 -165
  75. package/src/smart-defaults.test.ts +0 -361
  76. package/src/steps/async-support.steps.ts +0 -400
  77. package/src/steps/audit-core-builder.steps.ts +0 -188
  78. package/src/steps/audit-onerror-tuple.steps.ts +0 -99
  79. package/src/steps/basic-usage.steps.ts +0 -245
  80. package/src/steps/developer-experience.steps.ts +0 -204
  81. package/src/steps/error-handling.steps.ts +0 -345
  82. package/src/steps/fluent-api.steps.ts +0 -255
  83. package/src/steps/http-methods.steps.ts +0 -331
  84. package/src/steps/interceptor.steps.ts +0 -260
  85. package/src/steps/lifecycle-events.steps.ts +0 -142
  86. package/src/steps/performance-reliability.steps.ts +0 -423
  87. package/src/steps/plugin-integration.steps.ts +0 -280
  88. package/src/steps/request-history.steps.ts +0 -276
  89. package/src/steps/response-delay.steps.ts +0 -88
  90. package/src/steps/route-key-format.steps.ts +0 -99
  91. package/src/steps/standalone-server.steps.ts +0 -233
  92. package/src/steps/state-concurrency.steps.ts +0 -739
  93. package/src/steps/stateful-workflows.steps.ts +0 -353
  94. package/src/types.ts +0 -36
@@ -1,291 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { schmock } from "./index.js";
3
-
4
- describe("mock.intercept()", () => {
5
- let originalFetch: typeof globalThis.fetch;
6
- let mock: Schmock.CallableMockInstance;
7
-
8
- beforeEach(() => {
9
- originalFetch = globalThis.fetch;
10
- globalThis.fetch = vi.fn().mockResolvedValue(new Response("real backend"));
11
- mock = schmock();
12
- });
13
-
14
- afterEach(() => {
15
- globalThis.fetch = originalFetch;
16
- });
17
-
18
- it("intercepts a matched fetch request and returns mocked response", async () => {
19
- mock("GET /api/users", [{ id: 1, name: "Alice" }]);
20
- const handle = mock.intercept();
21
-
22
- const res = await fetch("http://localhost/api/users");
23
- expect(res.status).toBe(200);
24
- expect(await res.json()).toEqual([{ id: 1, name: "Alice" }]);
25
-
26
- handle.restore();
27
- });
28
-
29
- it("passes through unmatched routes when passthrough is true", async () => {
30
- mock("GET /api/users", [{ id: 1 }]);
31
- // Save reference to the vi.fn() mock that the interceptor will call on passthrough
32
- const mockFetch = globalThis.fetch as ReturnType<typeof vi.fn>;
33
- const handle = mock.intercept({ passthrough: true });
34
-
35
- await fetch("http://localhost/api/other");
36
- // The interceptor saves the vi.fn() as its original and calls it on passthrough
37
- expect(mockFetch).toHaveBeenCalled();
38
- handle.restore();
39
- });
40
-
41
- it("returns 404 when passthrough is disabled and route not found", async () => {
42
- mock("GET /api/users", [{ id: 1 }]);
43
- const handle = mock.intercept({ passthrough: false });
44
-
45
- const res = await fetch("http://localhost/api/other");
46
- expect(res.status).toBe(404);
47
-
48
- handle.restore();
49
- });
50
-
51
- it("restores original fetch", () => {
52
- const savedFetch = globalThis.fetch;
53
- const handle = mock.intercept();
54
-
55
- expect(globalThis.fetch).not.toBe(savedFetch);
56
- handle.restore();
57
- expect(globalThis.fetch).toBe(savedFetch);
58
- });
59
-
60
- it("reports active status", () => {
61
- const handle = mock.intercept();
62
- expect(handle.active).toBe(true);
63
-
64
- handle.restore();
65
- expect(handle.active).toBe(false);
66
- });
67
-
68
- it("filters by baseUrl", async () => {
69
- mock("GET /api/users", [{ id: 1 }]);
70
- const savedFetch = globalThis.fetch;
71
- const handle = mock.intercept({ baseUrl: "/api" });
72
-
73
- await fetch("http://localhost/other/path");
74
- // Should have called the saved fetch (passthrough for non-matching baseUrl)
75
- expect(savedFetch).toHaveBeenCalled();
76
-
77
- handle.restore();
78
- });
79
-
80
- it("throws when intercepting twice", () => {
81
- const handle = mock.intercept();
82
- expect(() => mock.intercept()).toThrow(/already intercepting/i);
83
- handle.restore();
84
- });
85
-
86
- it("applies beforeRequest hook", async () => {
87
- mock("GET /api/users", ({ headers }) => [
88
- 200,
89
- { token: headers["x-token"] },
90
- ]);
91
- const handle = mock.intercept({
92
- beforeRequest: (req) => ({
93
- ...req,
94
- headers: { ...req.headers, "x-token": "injected" },
95
- }),
96
- });
97
-
98
- const res = await fetch("http://localhost/api/users");
99
- expect(await res.json()).toEqual({ token: "injected" });
100
-
101
- handle.restore();
102
- });
103
-
104
- it("applies beforeResponse hook", async () => {
105
- mock("GET /api/users", [{ id: 1 }]);
106
- const handle = mock.intercept({
107
- beforeResponse: (resp) => ({
108
- ...resp,
109
- headers: { ...resp.headers, "x-mock": "true" },
110
- }),
111
- });
112
-
113
- const res = await fetch("http://localhost/api/users");
114
- expect(res.headers.get("x-mock")).toBe("true");
115
-
116
- handle.restore();
117
- });
118
-
119
- it("handles relative URLs", async () => {
120
- mock("GET /api/users", [{ id: 1 }]);
121
- const handle = mock.intercept();
122
-
123
- const res = await fetch("/api/users");
124
- expect(res.status).toBe(200);
125
- expect(await res.json()).toEqual([{ id: 1 }]);
126
-
127
- handle.restore();
128
- });
129
-
130
- it("applies errorFormatter when beforeRequest throws", async () => {
131
- mock("GET /api/users", [{ id: 1 }]);
132
- const handle = mock.intercept({
133
- beforeRequest: () => {
134
- throw new Error("hook failed");
135
- },
136
- errorFormatter: (err) => ({ custom: err.message }),
137
- });
138
-
139
- const res = await fetch("http://localhost/api/users");
140
- expect(res.status).toBe(500);
141
- expect(await res.json()).toEqual({ custom: "hook failed" });
142
-
143
- handle.restore();
144
- });
145
-
146
- it("normalizes header keys to lowercase", async () => {
147
- mock("POST /api/data", ({ headers }) => [
148
- 200,
149
- { auth: headers.authorization, ct: headers["content-type"] },
150
- ]);
151
- const handle = mock.intercept();
152
-
153
- const res = await fetch("http://localhost/api/data", {
154
- method: "POST",
155
- headers: {
156
- Authorization: "Bearer tok",
157
- "Content-Type": "application/json",
158
- },
159
- body: JSON.stringify({}),
160
- });
161
- expect(await res.json()).toEqual({
162
- auth: "Bearer tok",
163
- ct: "application/json",
164
- });
165
-
166
- handle.restore();
167
- });
168
-
169
- it("parses JSON body from fetch init", async () => {
170
- mock("POST /api/users", ({ body }) => [201, body]);
171
- const handle = mock.intercept();
172
-
173
- const res = await fetch("http://localhost/api/users", {
174
- method: "POST",
175
- headers: { "content-type": "application/json" },
176
- body: JSON.stringify({ name: "Alice" }),
177
- });
178
- expect(res.status).toBe(201);
179
- expect(await res.json()).toEqual({ name: "Alice" });
180
-
181
- handle.restore();
182
- });
183
-
184
- it("intercepts fetch called with a Request object", async () => {
185
- mock("GET /api/users", [{ id: 1, name: "Alice" }]);
186
- const handle = mock.intercept();
187
-
188
- const req = new Request("http://localhost/api/users");
189
- const res = await fetch(req);
190
- expect(res.status).toBe(200);
191
- expect(await res.json()).toEqual([{ id: 1, name: "Alice" }]);
192
-
193
- handle.restore();
194
- });
195
-
196
- it("prefers init.headers over Request.headers", async () => {
197
- mock("GET /api/data", ({ headers }) => [200, { val: headers["x-custom"] }]);
198
- const handle = mock.intercept();
199
-
200
- const req = new Request("http://localhost/api/data", {
201
- headers: { "X-Custom": "from-request" },
202
- });
203
- const res = await fetch(req, {
204
- headers: { "X-Custom": "from-init" },
205
- });
206
- expect(await res.json()).toEqual({ val: "from-init" });
207
-
208
- handle.restore();
209
- });
210
-
211
- it("parses URLSearchParams body", async () => {
212
- mock("POST /api/form", ({ body }) => [200, body]);
213
- const handle = mock.intercept();
214
-
215
- const params = new URLSearchParams();
216
- params.set("name", "Alice");
217
- params.set("role", "admin");
218
-
219
- const res = await fetch("http://localhost/api/form", {
220
- method: "POST",
221
- body: params,
222
- });
223
- expect(await res.json()).toEqual({ name: "Alice", role: "admin" });
224
-
225
- handle.restore();
226
- });
227
-
228
- it("baseUrl /api should NOT match /apiv2 (segment boundary)", async () => {
229
- mock("GET /apiv2/data", [{ v2: true }]);
230
- const savedFetch = globalThis.fetch;
231
- const handle = mock.intercept({ baseUrl: "/api" });
232
-
233
- // /apiv2/data does not start with "/api/" — it should passthrough
234
- await fetch("http://localhost/apiv2/data");
235
- expect(savedFetch).toHaveBeenCalled();
236
-
237
- handle.restore();
238
- });
239
-
240
- it("baseUrl with trailing slash matches the same routes as without", async () => {
241
- mock("GET /api/items", [{ id: 1 }]);
242
-
243
- // Without trailing slash
244
- const handle1 = mock.intercept({ baseUrl: "/api" });
245
- const res1 = await fetch("http://localhost/api/items");
246
- expect(res1.status).toBe(200);
247
- expect(await res1.json()).toEqual([{ id: 1 }]);
248
- handle1.restore();
249
-
250
- // With trailing slash — should still match /api/items
251
- const handle2 = mock.intercept({ baseUrl: "/api/" });
252
- const res2 = await fetch("http://localhost/api/items");
253
- expect(res2.status).toBe(200);
254
- expect(await res2.json()).toEqual([{ id: 1 }]);
255
- handle2.restore();
256
- });
257
-
258
- it("extractBody: init.body wins over Request.body per Fetch spec", async () => {
259
- mock("POST /api/data", ({ body }) => [200, body]);
260
- const handle = mock.intercept();
261
-
262
- const req = new Request("http://localhost/api/data", {
263
- method: "POST",
264
- headers: { "content-type": "application/json" },
265
- body: JSON.stringify({ source: "request" }),
266
- });
267
-
268
- const res = await fetch(req, {
269
- body: JSON.stringify({ source: "init" }),
270
- });
271
- expect(await res.json()).toEqual({ source: "init" });
272
-
273
- handle.restore();
274
- });
275
-
276
- it("extractBody: failed JSON parse falls back to text", async () => {
277
- mock("POST /api/text", ({ body }) => [200, { received: body }]);
278
- const handle = mock.intercept();
279
-
280
- const res = await fetch("http://localhost/api/text", {
281
- method: "POST",
282
- headers: { "content-type": "text/plain" },
283
- body: "not-json-{broken",
284
- });
285
-
286
- const json = await res.json();
287
- expect(json.received).toBe("not-json-{broken");
288
-
289
- handle.restore();
290
- });
291
- });
@@ -1,320 +0,0 @@
1
- /// <reference path="../schmock.d.ts" />
2
-
3
- import { isRouteNotFound, toHttpMethod } from "./constants.js";
4
-
5
- /**
6
- * Extract pathname from a URL string, handling both absolute and relative URLs.
7
- */
8
- function extractPathname(url: string): string {
9
- const queryStart = url.indexOf("?");
10
- const urlWithoutQuery = queryStart === -1 ? url : url.slice(0, queryStart);
11
-
12
- if (urlWithoutQuery.includes("://")) {
13
- try {
14
- return new URL(urlWithoutQuery).pathname;
15
- } catch {
16
- // Fall through to simple extraction
17
- }
18
- }
19
-
20
- if (!urlWithoutQuery.startsWith("/")) {
21
- return `/${urlWithoutQuery}`;
22
- }
23
-
24
- return urlWithoutQuery;
25
- }
26
-
27
- /**
28
- * Extract origin (scheme + host + port) from a URL string, or null for
29
- * relative URLs that don't carry one.
30
- */
31
- function extractOrigin(url: string): string | null {
32
- if (!url.includes("://")) return null;
33
- try {
34
- return new URL(url).origin;
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- /**
41
- * Parse the user-supplied baseUrl option into its origin and path parts.
42
- * - "/api" → { origin: null, path: "/api" }
43
- * - "https://x.com/api/v1" → { origin: "https://x.com", path: "/api/v1" }
44
- * - "https://x.com" → { origin: "https://x.com", path: "" }
45
- *
46
- * Trailing slash is stripped from the path so the segment-boundary check
47
- * works the same way for "/api" and "/api/".
48
- */
49
- function parseBaseUrl(baseUrl: string): {
50
- origin: string | null;
51
- path: string;
52
- } {
53
- if (baseUrl.includes("://")) {
54
- try {
55
- const parsed = new URL(baseUrl);
56
- const rawPath = parsed.pathname;
57
- const path = rawPath === "/" ? "" : rawPath.replace(/\/$/, "");
58
- return { origin: parsed.origin, path };
59
- } catch {
60
- // Fall through to path-only handling
61
- }
62
- }
63
- return { origin: null, path: baseUrl.replace(/\/$/, "") };
64
- }
65
-
66
- /**
67
- * Extract query parameters from a URL string.
68
- */
69
- function extractQuery(url: string): Record<string, string> {
70
- const queryStart = url.indexOf("?");
71
- if (queryStart === -1) return {};
72
-
73
- const params = new URLSearchParams(url.slice(queryStart + 1));
74
- const result: Record<string, string> = {};
75
- params.forEach((value, key) => {
76
- result[key] = value;
77
- });
78
- return result;
79
- }
80
-
81
- /**
82
- * Extract headers from fetch init or Request object.
83
- */
84
- function extractHeaders(
85
- input: RequestInfo | URL,
86
- init?: RequestInit,
87
- ): Record<string, string> {
88
- const headers: Record<string, string> = {};
89
-
90
- const raw =
91
- init?.headers ?? (input instanceof Request ? input.headers : undefined);
92
- if (!raw) return headers;
93
-
94
- if (raw instanceof Headers) {
95
- raw.forEach((value, key) => {
96
- headers[key.toLowerCase()] = value;
97
- });
98
- } else if (Array.isArray(raw)) {
99
- for (const [key, value] of raw) {
100
- headers[key.toLowerCase()] = value;
101
- }
102
- } else {
103
- for (const key of Object.keys(raw)) {
104
- headers[key.toLowerCase()] = (raw as Record<string, string>)[key];
105
- }
106
- }
107
-
108
- return headers;
109
- }
110
-
111
- /**
112
- * Extract body from fetch init, parsing JSON when possible.
113
- */
114
- async function extractBody(
115
- input: RequestInfo | URL,
116
- init?: RequestInit,
117
- ): Promise<unknown> {
118
- // Per Fetch spec, init.body overrides Request.body when both are present
119
- const bodyInit = init?.body ?? (input instanceof Request ? input.body : null);
120
- if (bodyInit === null || bodyInit === undefined) return undefined;
121
-
122
- // String body — try to parse as JSON
123
- if (typeof bodyInit === "string") {
124
- try {
125
- return JSON.parse(bodyInit);
126
- } catch {
127
- return bodyInit;
128
- }
129
- }
130
-
131
- // URLSearchParams — convert to key/value object
132
- if (bodyInit instanceof URLSearchParams) {
133
- const result: Record<string, string> = {};
134
- bodyInit.forEach((value, key) => {
135
- result[key] = value;
136
- });
137
- return result;
138
- }
139
-
140
- // Request with body — clone and read
141
- if (input instanceof Request && !init?.body && input.body) {
142
- try {
143
- return await input.clone().json();
144
- } catch {
145
- try {
146
- return await input.clone().text();
147
- } catch {
148
- return undefined;
149
- }
150
- }
151
- }
152
-
153
- return undefined;
154
- }
155
-
156
- /**
157
- * Create a fetch interceptor that routes requests through mock.handle().
158
- */
159
- export function createFetchInterceptor(
160
- handle: (
161
- method: Schmock.HttpMethod,
162
- path: string,
163
- requestOptions?: Schmock.RequestOptions,
164
- ) => Promise<Schmock.Response>,
165
- options: Schmock.InterceptOptions = {},
166
- ): Schmock.InterceptHandle {
167
- const {
168
- baseUrl,
169
- passthrough = true,
170
- beforeRequest,
171
- beforeResponse,
172
- errorFormatter,
173
- } = options;
174
-
175
- const originalFetch = globalThis.fetch;
176
- let active = true;
177
-
178
- globalThis.fetch = async (
179
- input: RequestInfo | URL,
180
- init?: RequestInit,
181
- ): Promise<Response> => {
182
- // Resolve the URL string
183
- const urlString =
184
- input instanceof Request
185
- ? input.url
186
- : input instanceof URL
187
- ? input.href
188
- : input;
189
-
190
- const path = extractPathname(urlString);
191
-
192
- // BaseUrl filter — non-matching requests go straight to real fetch.
193
- // Two modes:
194
- // - origin form ("https://api.example.com/v1"): require matching
195
- // origin AND matching path prefix.
196
- // - path form ("/api"): match pathname prefix only.
197
- // Both enforce a segment boundary so "/api" doesn't match "/apiv2".
198
- if (baseUrl) {
199
- const { origin: baseOrigin, path: basePath } = parseBaseUrl(baseUrl);
200
- if (baseOrigin) {
201
- const reqOrigin = extractOrigin(urlString);
202
- if (reqOrigin !== baseOrigin) {
203
- return originalFetch(input, init);
204
- }
205
- }
206
- if (basePath) {
207
- const isMatch = path === basePath || path.startsWith(`${basePath}/`);
208
- if (!isMatch) {
209
- return originalFetch(input, init);
210
- }
211
- }
212
- }
213
-
214
- // Build adapter request
215
- const method =
216
- input instanceof Request ? input.method : (init?.method ?? "GET");
217
- const headers = extractHeaders(input, init);
218
- const query = extractQuery(urlString);
219
- const body = await extractBody(input, init);
220
-
221
- let adapterRequest: Schmock.AdapterRequest = {
222
- method,
223
- path,
224
- headers,
225
- body,
226
- query,
227
- };
228
-
229
- try {
230
- // Apply beforeRequest hook
231
- if (beforeRequest) {
232
- const modified = await beforeRequest(adapterRequest);
233
- if (modified) {
234
- adapterRequest = modified;
235
- }
236
- }
237
-
238
- const schmockResponse = await handle(
239
- toHttpMethod(adapterRequest.method),
240
- adapterRequest.path,
241
- {
242
- headers: adapterRequest.headers,
243
- body: adapterRequest.body,
244
- query: adapterRequest.query,
245
- },
246
- );
247
-
248
- // Route not found — passthrough or 404
249
- if (isRouteNotFound(schmockResponse)) {
250
- if (passthrough) {
251
- return originalFetch(input, init);
252
- }
253
- return new Response(
254
- JSON.stringify({
255
- error: "No matching mock route found",
256
- code: "ROUTE_NOT_FOUND",
257
- }),
258
- {
259
- status: 404,
260
- headers: { "content-type": "application/json" },
261
- },
262
- );
263
- }
264
-
265
- // Apply beforeResponse hook
266
- let response: Schmock.AdapterResponse = schmockResponse;
267
- if (beforeResponse) {
268
- const modified = await beforeResponse(response, adapterRequest);
269
- if (modified) {
270
- response = modified;
271
- }
272
- }
273
-
274
- // Build fetch Response
275
- const responseHeaders = new Headers(response.headers);
276
- if (
277
- !responseHeaders.has("content-type") &&
278
- response.body !== null &&
279
- response.body !== undefined
280
- ) {
281
- responseHeaders.set("content-type", "application/json");
282
- }
283
-
284
- const responseBody =
285
- response.body === null || response.body === undefined
286
- ? null
287
- : typeof response.body === "string"
288
- ? response.body
289
- : JSON.stringify(response.body);
290
-
291
- return new Response(responseBody, {
292
- status: response.status,
293
- headers: responseHeaders,
294
- });
295
- } catch (error) {
296
- if (errorFormatter) {
297
- const formatted = errorFormatter(
298
- error instanceof Error ? error : new Error(String(error)),
299
- );
300
- return new Response(JSON.stringify(formatted), {
301
- status: 500,
302
- headers: { "content-type": "application/json" },
303
- });
304
- }
305
- throw error;
306
- }
307
- };
308
-
309
- return {
310
- restore() {
311
- if (active) {
312
- globalThis.fetch = originalFetch;
313
- active = false;
314
- }
315
- },
316
- get active() {
317
- return active;
318
- },
319
- };
320
- }