1 | const { debug, setDebugPhase, getDebug } = require('./debug');
|
2 | const responseBuilder = require('./response-builder');
|
3 | const requestUtils = require('./request-utils');
|
4 | const FetchMock = {};
|
5 |
|
6 |
|
7 |
|
8 | class AbortError extends Error {
|
9 | constructor() {
|
10 | super(...arguments);
|
11 | this.name = 'AbortError';
|
12 | this.message = 'The operation was aborted.';
|
13 |
|
14 |
|
15 | if (Error.captureStackTrace) {
|
16 | Error.captureStackTrace(this, this.constructor);
|
17 | }
|
18 | }
|
19 | }
|
20 |
|
21 | const resolve = async (
|
22 | { response, responseIsFetch = false },
|
23 | url,
|
24 | options,
|
25 | request
|
26 | ) => {
|
27 | const debug = getDebug('resolve()');
|
28 | debug('Recursively resolving function and promise responses');
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 | while (true) {
|
37 | if (typeof response === 'function') {
|
38 | debug(' Response is a function');
|
39 |
|
40 |
|
41 | if (responseIsFetch) {
|
42 | if (request) {
|
43 | debug(' -> Calling fetch with Request instance');
|
44 | return response(request);
|
45 | }
|
46 | debug(' -> Calling fetch with url and options');
|
47 | return response(url, options);
|
48 | } else {
|
49 | debug(' -> Calling response function');
|
50 | response = response(url, options, request);
|
51 | }
|
52 | } else if (typeof response.then === 'function') {
|
53 | debug(' Response is a promise');
|
54 | debug(' -> Resolving promise');
|
55 | response = await response;
|
56 | } else {
|
57 | debug(' Response is not a function or a promise');
|
58 | debug(' -> Exiting response resolution recursion');
|
59 | return response;
|
60 | }
|
61 | }
|
62 | };
|
63 |
|
64 | FetchMock.fetchHandler = function (url, options, request) {
|
65 | setDebugPhase('handle');
|
66 | const debug = getDebug('fetchHandler()');
|
67 | debug('fetch called with:', url, options);
|
68 | const normalizedRequest = requestUtils.normalizeRequest(
|
69 | url,
|
70 | options,
|
71 | this.config.Request
|
72 | );
|
73 |
|
74 | ({ url, options, request } = normalizedRequest);
|
75 |
|
76 | const { signal } = normalizedRequest;
|
77 |
|
78 | debug('Request normalised');
|
79 | debug(' url', url);
|
80 | debug(' options', options);
|
81 | debug(' request', request);
|
82 | debug(' signal', signal);
|
83 |
|
84 | if (request && this.routes.some(({ usesBody }) => usesBody)) {
|
85 | debug(
|
86 | 'Need to wait for Body to be streamed before calling router: switching to async mode'
|
87 | );
|
88 | return this._asyncFetchHandler(url, options, request, signal);
|
89 | }
|
90 | return this._fetchHandler(url, options, request, signal);
|
91 | };
|
92 |
|
93 | FetchMock._asyncFetchHandler = async function (url, options, request, signal) {
|
94 | options.body = await options.body;
|
95 | return this._fetchHandler(url, options, request, signal);
|
96 | };
|
97 |
|
98 | FetchMock._fetchHandler = function (url, options, request, signal) {
|
99 | const route = this.executeRouter(url, options, request);
|
100 |
|
101 |
|
102 | let done;
|
103 | this._holdingPromises.push(new this.config.Promise((res) => (done = res)));
|
104 |
|
105 |
|
106 |
|
107 | return new this.config.Promise((res, rej) => {
|
108 | if (signal) {
|
109 | debug('signal exists - enabling fetch abort');
|
110 | const abort = () => {
|
111 | debug('aborting fetch');
|
112 |
|
113 | rej(
|
114 | typeof DOMException !== 'undefined'
|
115 | ? new DOMException('The operation was aborted.', 'AbortError')
|
116 | : new AbortError()
|
117 | );
|
118 | done();
|
119 | };
|
120 | if (signal.aborted) {
|
121 | debug('signal is already aborted - aborting the fetch');
|
122 | abort();
|
123 | }
|
124 | signal.addEventListener('abort', abort);
|
125 | }
|
126 |
|
127 | this.generateResponse(route, url, options, request)
|
128 | .then(res, rej)
|
129 | .then(done, done)
|
130 | .then(() => {
|
131 | setDebugPhase();
|
132 | });
|
133 | });
|
134 | };
|
135 |
|
136 | FetchMock.fetchHandler.isMock = true;
|
137 |
|
138 | FetchMock.executeRouter = function (url, options, request) {
|
139 | const debug = getDebug('executeRouter()');
|
140 | debug(`Attempting to match request to a route`);
|
141 | if (this.getOption('fallbackToNetwork') === 'always') {
|
142 | debug(
|
143 | ' Configured with fallbackToNetwork=always - passing through to fetch'
|
144 | );
|
145 | return { response: this.getNativeFetch(), responseIsFetch: true };
|
146 | }
|
147 |
|
148 | const match = this.router(url, options, request);
|
149 |
|
150 | if (match) {
|
151 | debug(' Matching route found');
|
152 | return match;
|
153 | }
|
154 |
|
155 | if (this.getOption('warnOnFallback')) {
|
156 | console.warn(`Unmatched ${(options && options.method) || 'GET'} to ${url}`);
|
157 | }
|
158 |
|
159 | this.push({ url, options, request, isUnmatched: true });
|
160 |
|
161 | if (this.fallbackResponse) {
|
162 | debug(' No matching route found - using fallbackResponse');
|
163 | return { response: this.fallbackResponse };
|
164 | }
|
165 |
|
166 | if (!this.getOption('fallbackToNetwork')) {
|
167 | throw new Error(
|
168 | `fetch-mock: No fallback response defined for ${
|
169 | (options && options.method) || 'GET'
|
170 | } to ${url}`
|
171 | );
|
172 | }
|
173 |
|
174 | debug(' Configured to fallbackToNetwork - passing through to fetch');
|
175 | return { response: this.getNativeFetch(), responseIsFetch: true };
|
176 | };
|
177 |
|
178 | FetchMock.generateResponse = async function (route, url, options, request) {
|
179 | const debug = getDebug('generateResponse()');
|
180 | const response = await resolve(route, url, options, request);
|
181 |
|
182 |
|
183 |
|
184 | if (response.throws && typeof response !== 'function') {
|
185 | debug('response.throws is defined - throwing an error');
|
186 | throw response.throws;
|
187 | }
|
188 |
|
189 |
|
190 | if (this.config.Response.prototype.isPrototypeOf(response)) {
|
191 | debug('response is already a Response instance - returning it');
|
192 | return response;
|
193 | }
|
194 |
|
195 |
|
196 | return responseBuilder({
|
197 | url,
|
198 | responseConfig: response,
|
199 | fetchMock: this,
|
200 | route,
|
201 | });
|
202 | };
|
203 |
|
204 | FetchMock.router = function (url, options, request) {
|
205 | const route = this.routes.find((route, i) => {
|
206 | debug(`Trying to match route ${i}`);
|
207 | return route.matcher(url, options, request);
|
208 | });
|
209 |
|
210 | if (route) {
|
211 | this.push({
|
212 | url,
|
213 | options,
|
214 | request,
|
215 | identifier: route.identifier,
|
216 | });
|
217 | return route;
|
218 | }
|
219 | };
|
220 |
|
221 | FetchMock.getNativeFetch = function () {
|
222 | const func = this.realFetch || (this.isSandbox && this.config.fetch);
|
223 | if (!func) {
|
224 | throw new Error(
|
225 | 'fetch-mock: Falling back to network only available on global fetch-mock, or by setting config.fetch on sandboxed fetch-mock'
|
226 | );
|
227 | }
|
228 | return func;
|
229 | };
|
230 |
|
231 | FetchMock.push = function ({ url, options, request, isUnmatched, identifier }) {
|
232 | debug('Recording fetch call', {
|
233 | url,
|
234 | options,
|
235 | request,
|
236 | isUnmatched,
|
237 | identifier,
|
238 | });
|
239 | const args = [url, options];
|
240 | args.request = request;
|
241 | args.identifier = identifier;
|
242 | args.isUnmatched = isUnmatched;
|
243 | this._calls.push(args);
|
244 | };
|
245 |
|
246 | module.exports = FetchMock;
|