@wordpress/api-fetch 7.29.1-next.f34ab90e9.0 → 7.30.1-next.836ecdcae.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/build/index.js +50 -51
  3. package/build/index.js.map +1 -1
  4. package/build/middlewares/fetch-all-middleware.js +14 -14
  5. package/build/middlewares/fetch-all-middleware.js.map +1 -1
  6. package/build/middlewares/http-v1.js +6 -5
  7. package/build/middlewares/http-v1.js.map +1 -1
  8. package/build/middlewares/media-upload.js +7 -7
  9. package/build/middlewares/media-upload.js.map +1 -1
  10. package/build/middlewares/namespace-endpoint.js +2 -1
  11. package/build/middlewares/namespace-endpoint.js.map +1 -1
  12. package/build/middlewares/nonce.js +7 -5
  13. package/build/middlewares/nonce.js.map +1 -1
  14. package/build/middlewares/preloading.js +10 -7
  15. package/build/middlewares/preloading.js.map +1 -1
  16. package/build/middlewares/root-url.js +2 -2
  17. package/build/middlewares/root-url.js.map +1 -1
  18. package/build/middlewares/theme-preview.js +6 -2
  19. package/build/middlewares/theme-preview.js.map +1 -1
  20. package/build/middlewares/user-locale.js +2 -1
  21. package/build/middlewares/user-locale.js.map +1 -1
  22. package/build/types.js.map +1 -1
  23. package/build/utils/response.js +31 -50
  24. package/build/utils/response.js.map +1 -1
  25. package/build-module/index.js +38 -52
  26. package/build-module/index.js.map +1 -1
  27. package/build-module/middlewares/fetch-all-middleware.js +14 -15
  28. package/build-module/middlewares/fetch-all-middleware.js.map +1 -1
  29. package/build-module/middlewares/http-v1.js +6 -5
  30. package/build-module/middlewares/http-v1.js.map +1 -1
  31. package/build-module/middlewares/media-upload.js +7 -8
  32. package/build-module/middlewares/media-upload.js.map +1 -1
  33. package/build-module/middlewares/namespace-endpoint.js +2 -1
  34. package/build-module/middlewares/namespace-endpoint.js.map +1 -1
  35. package/build-module/middlewares/nonce.js +7 -5
  36. package/build-module/middlewares/nonce.js.map +1 -1
  37. package/build-module/middlewares/preloading.js +10 -7
  38. package/build-module/middlewares/preloading.js.map +1 -1
  39. package/build-module/middlewares/root-url.js +3 -2
  40. package/build-module/middlewares/root-url.js.map +1 -1
  41. package/build-module/middlewares/theme-preview.js +5 -2
  42. package/build-module/middlewares/theme-preview.js.map +1 -1
  43. package/build-module/middlewares/user-locale.js +2 -1
  44. package/build-module/middlewares/user-locale.js.map +1 -1
  45. package/build-module/types.js.map +1 -1
  46. package/build-module/utils/response.js +30 -48
  47. package/build-module/utils/response.js.map +1 -1
  48. package/build-types/index.d.ts +25 -32
  49. package/build-types/index.d.ts.map +1 -1
  50. package/build-types/middlewares/fetch-all-middleware.d.ts +5 -4
  51. package/build-types/middlewares/fetch-all-middleware.d.ts.map +1 -1
  52. package/build-types/middlewares/http-v1.d.ts +8 -3
  53. package/build-types/middlewares/http-v1.d.ts.map +1 -1
  54. package/build-types/middlewares/media-upload.d.ts +5 -4
  55. package/build-types/middlewares/media-upload.d.ts.map +1 -1
  56. package/build-types/middlewares/namespace-endpoint.d.ts +4 -3
  57. package/build-types/middlewares/namespace-endpoint.d.ts.map +1 -1
  58. package/build-types/middlewares/nonce.d.ts +9 -4
  59. package/build-types/middlewares/nonce.d.ts.map +1 -1
  60. package/build-types/middlewares/preloading.d.ts +8 -4
  61. package/build-types/middlewares/preloading.d.ts.map +1 -1
  62. package/build-types/middlewares/root-url.d.ts +8 -4
  63. package/build-types/middlewares/root-url.d.ts.map +1 -1
  64. package/build-types/middlewares/theme-preview.d.ts +8 -4
  65. package/build-types/middlewares/theme-preview.d.ts.map +1 -1
  66. package/build-types/middlewares/user-locale.d.ts +4 -3
  67. package/build-types/middlewares/user-locale.d.ts.map +1 -1
  68. package/build-types/types.d.ts +4 -3
  69. package/build-types/types.d.ts.map +1 -1
  70. package/build-types/utils/response.d.ts +13 -5
  71. package/build-types/utils/response.d.ts.map +1 -1
  72. package/package.json +4 -4
  73. package/src/{index.js → index.ts} +78 -69
  74. package/src/middlewares/{fetch-all-middleware.js → fetch-all-middleware.ts} +24 -20
  75. package/src/middlewares/{http-v1.js → http-v1.ts} +8 -6
  76. package/src/middlewares/{media-upload.js → media-upload.ts} +13 -12
  77. package/src/middlewares/{namespace-endpoint.js → namespace-endpoint.ts} +7 -2
  78. package/src/middlewares/{nonce.js → nonce.ts} +15 -7
  79. package/src/middlewares/{preloading.js → preloading.ts} +21 -11
  80. package/src/middlewares/root-url.ts +47 -0
  81. package/src/middlewares/test/{fetch-all-middleware.js → fetch-all-middleware.ts} +1 -1
  82. package/src/middlewares/test/{http-v1.js → http-v1.ts} +7 -4
  83. package/src/middlewares/test/{media-upload.js → media-upload.ts} +2 -1
  84. package/src/middlewares/test/{namespace-endpoint.js → namespace-endpoint.ts} +2 -1
  85. package/src/middlewares/test/{nonce.js → nonce.ts} +5 -4
  86. package/src/middlewares/test/{preloading.js → preloading.ts} +31 -11
  87. package/src/middlewares/test/{root-url.js → root-url.ts} +2 -1
  88. package/src/middlewares/test/{user-locale.js → user-locale.ts} +9 -8
  89. package/src/middlewares/theme-preview.ts +60 -0
  90. package/src/middlewares/{user-locale.js → user-locale.ts} +4 -2
  91. package/src/test/index.js +218 -147
  92. package/src/types.ts +10 -5
  93. package/src/utils/response.ts +64 -0
  94. package/tsconfig.tsbuildinfo +1 -1
  95. package/src/middlewares/root-url.js +0 -44
  96. package/src/middlewares/theme-preview.js +0 -42
  97. package/src/utils/response.js +0 -85
package/src/test/index.js CHANGED
@@ -1,56 +1,57 @@
1
1
  /**
2
- * Internal dependencies
3
- */
4
- import apiFetch from '../';
5
-
6
- /**
7
- * Mock return value for a successful fetch JSON return value.
2
+ * Mock response value for a successful fetch.
8
3
  *
9
- * @return {Promise} Mock return value.
4
+ * @return {Response} Mock return value.
10
5
  */
11
- const DEFAULT_FETCH_MOCK_RETURN = Promise.resolve( {
6
+ const DEFAULT_FETCH_MOCK_RETURN = {
7
+ ok: true,
12
8
  status: 200,
13
9
  json: () => Promise.resolve( {} ),
14
- } );
10
+ };
15
11
 
16
12
  describe( 'apiFetch', () => {
17
- const originalFetch = window.fetch;
13
+ let apiFetch;
14
+ const originalFetch = globalThis.fetch;
15
+
16
+ beforeEach( async () => {
17
+ // Reset the `apiFetch` module before each test to clear
18
+ // internal variables (middlewares, fetch handler, etc.).
19
+ jest.resetModules();
20
+ apiFetch = ( await import( '../' ) ).default;
18
21
 
19
- beforeEach( () => {
20
- window.fetch = jest.fn();
22
+ globalThis.fetch = jest.fn();
21
23
  } );
22
24
 
23
25
  afterAll( () => {
24
- window.fetch = originalFetch;
26
+ globalThis.fetch = originalFetch;
25
27
  } );
26
28
 
27
- it( 'should call the API properly', () => {
28
- window.fetch.mockReturnValue(
29
- Promise.resolve( {
30
- status: 200,
31
- json() {
32
- return Promise.resolve( { message: 'ok' } );
33
- },
34
- } )
35
- );
29
+ it( 'should call the API properly', async () => {
30
+ globalThis.fetch.mockResolvedValue( {
31
+ ok: true,
32
+ status: 200,
33
+ async json() {
34
+ return { message: 'ok' };
35
+ },
36
+ } );
36
37
 
37
- return apiFetch( { path: '/random' } ).then( ( body ) => {
38
- expect( body ).toEqual( { message: 'ok' } );
38
+ await expect( apiFetch( { path: '/random' } ) ).resolves.toEqual( {
39
+ message: 'ok',
39
40
  } );
40
41
  } );
41
42
 
42
- it( 'should fetch with non-JSON body', () => {
43
- window.fetch.mockReturnValue( DEFAULT_FETCH_MOCK_RETURN );
43
+ it( 'should fetch with non-JSON body', async () => {
44
+ globalThis.fetch.mockResolvedValue( DEFAULT_FETCH_MOCK_RETURN );
44
45
 
45
46
  const body = 'FormData';
46
47
 
47
- apiFetch( {
48
+ await apiFetch( {
48
49
  path: '/wp/v2/media',
49
50
  method: 'POST',
50
51
  body,
51
52
  } );
52
53
 
53
- expect( window.fetch ).toHaveBeenCalledWith(
54
+ expect( globalThis.fetch ).toHaveBeenCalledWith(
54
55
  '/wp/v2/media?_locale=user',
55
56
  {
56
57
  credentials: 'include',
@@ -63,10 +64,10 @@ describe( 'apiFetch', () => {
63
64
  );
64
65
  } );
65
66
 
66
- it( 'should fetch with a JSON body', () => {
67
- window.fetch.mockReturnValue( DEFAULT_FETCH_MOCK_RETURN );
67
+ it( 'should fetch with a JSON body', async () => {
68
+ globalThis.fetch.mockResolvedValue( DEFAULT_FETCH_MOCK_RETURN );
68
69
 
69
- apiFetch( {
70
+ await apiFetch( {
70
71
  path: '/wp/v2/posts',
71
72
  method: 'POST',
72
73
  headers: {
@@ -75,7 +76,7 @@ describe( 'apiFetch', () => {
75
76
  data: {},
76
77
  } );
77
78
 
78
- expect( window.fetch ).toHaveBeenCalledWith(
79
+ expect( globalThis.fetch ).toHaveBeenCalledWith(
79
80
  '/wp/v2/posts?_locale=user',
80
81
  {
81
82
  body: '{}',
@@ -89,17 +90,17 @@ describe( 'apiFetch', () => {
89
90
  );
90
91
  } );
91
92
 
92
- it( 'should respect developer-provided options', () => {
93
- window.fetch.mockReturnValue( DEFAULT_FETCH_MOCK_RETURN );
93
+ it( 'should respect developer-provided options', async () => {
94
+ globalThis.fetch.mockResolvedValue( DEFAULT_FETCH_MOCK_RETURN );
94
95
 
95
- apiFetch( {
96
+ await apiFetch( {
96
97
  path: '/wp/v2/posts',
97
98
  method: 'POST',
98
99
  data: {},
99
100
  credentials: 'omit',
100
101
  } );
101
102
 
102
- expect( window.fetch ).toHaveBeenCalledWith(
103
+ expect( globalThis.fetch ).toHaveBeenCalledWith(
103
104
  '/wp/v2/posts?_locale=user',
104
105
  {
105
106
  body: '{}',
@@ -113,83 +114,86 @@ describe( 'apiFetch', () => {
113
114
  );
114
115
  } );
115
116
 
116
- it( 'should return the error message properly', () => {
117
- window.fetch.mockReturnValue(
118
- Promise.resolve( {
119
- status: 400,
120
- json() {
121
- return Promise.resolve( {
122
- code: 'bad_request',
123
- message: 'Bad Request',
124
- } );
125
- },
126
- } )
127
- );
117
+ it( 'should return the error message properly', async () => {
118
+ globalThis.fetch.mockResolvedValue( {
119
+ ok: false,
120
+ status: 400,
121
+ async json() {
122
+ return {
123
+ code: 'bad_request',
124
+ message: 'Bad Request',
125
+ };
126
+ },
127
+ } );
128
128
 
129
- return apiFetch( { path: '/random' } ).catch( ( body ) => {
130
- // eslint-disable-next-line jest/no-conditional-expect
131
- expect( body ).toEqual( {
132
- code: 'bad_request',
133
- message: 'Bad Request',
134
- } );
129
+ await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
130
+ code: 'bad_request',
131
+ message: 'Bad Request',
135
132
  } );
136
133
  } );
137
134
 
138
- it( 'should return invalid JSON error if no json response', () => {
139
- window.fetch.mockReturnValue(
140
- Promise.resolve( {
141
- status: 200,
142
- } )
143
- );
135
+ it( 'should return invalid JSON error if no json response', async () => {
136
+ globalThis.fetch.mockResolvedValue( {
137
+ ok: true,
138
+ status: 200,
139
+ async json() {
140
+ return JSON.parse( '' );
141
+ },
142
+ } );
144
143
 
145
- return apiFetch( { path: '/random' } ).catch( ( body ) => {
146
- // eslint-disable-next-line jest/no-conditional-expect
147
- expect( body ).toEqual( {
148
- code: 'invalid_json',
149
- message: 'The response is not a valid JSON response.',
150
- } );
144
+ await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
145
+ code: 'invalid_json',
146
+ message: 'The response is not a valid JSON response.',
151
147
  } );
152
148
  } );
153
149
 
154
- it( 'should return invalid JSON error if response is not valid', () => {
155
- window.fetch.mockReturnValue(
156
- Promise.resolve( {
157
- status: 200,
158
- json() {
159
- return Promise.reject();
160
- },
161
- } )
162
- );
150
+ it( 'should return invalid JSON error if response is not valid', async () => {
151
+ globalThis.fetch.mockResolvedValue( {
152
+ ok: true,
153
+ status: 200,
154
+ async json() {
155
+ return JSON.parse( '' );
156
+ },
157
+ } );
163
158
 
164
- return apiFetch( { path: '/random' } ).catch( ( body ) => {
165
- // eslint-disable-next-line jest/no-conditional-expect
166
- expect( body ).toEqual( {
167
- code: 'invalid_json',
168
- message: 'The response is not a valid JSON response.',
169
- } );
159
+ await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
160
+ code: 'invalid_json',
161
+ message: 'The response is not a valid JSON response.',
170
162
  } );
171
163
  } );
172
164
 
173
- it( 'should return offline error when fetch errors', () => {
174
- window.fetch.mockReturnValue( Promise.reject() );
165
+ it( 'should return offline error when fetch errors', async () => {
166
+ globalThis.fetch.mockRejectedValue(
167
+ new TypeError( 'Failed to fetch' )
168
+ );
175
169
 
176
- return apiFetch( { path: '/random' } ).catch( ( body ) => {
177
- // eslint-disable-next-line jest/no-conditional-expect
178
- expect( body ).toEqual( {
179
- code: 'fetch_error',
180
- message: 'You are probably offline.',
181
- } );
170
+ await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
171
+ code: 'fetch_error',
172
+ message: 'Could not get a valid response from the server.',
182
173
  } );
183
174
  } );
184
175
 
185
176
  it( 'should throw AbortError when fetch aborts', async () => {
186
- const abortError = new Error();
187
- abortError.name = 'AbortError';
188
- abortError.code = 20;
189
-
190
- window.fetch.mockReturnValue( Promise.reject( abortError ) );
177
+ globalThis.fetch.mockImplementation(
178
+ ( path, { signal } ) =>
179
+ new Promise( ( _, reject ) => {
180
+ if ( ! signal ) {
181
+ reject( new Error( 'Expected signal as argument' ) );
182
+ return;
183
+ }
184
+
185
+ signal.throwIfAborted();
186
+ signal.addEventListener(
187
+ 'abort',
188
+ ( e ) => {
189
+ reject( e.target.reason );
190
+ },
191
+ { once: true }
192
+ );
193
+ } )
194
+ );
191
195
 
192
- const controller = new window.AbortController();
196
+ const controller = new globalThis.AbortController();
193
197
 
194
198
  const promise = apiFetch( {
195
199
  path: '/random',
@@ -198,85 +202,149 @@ describe( 'apiFetch', () => {
198
202
 
199
203
  controller.abort();
200
204
 
201
- let error;
205
+ await expect( promise ).rejects.toMatchObject( {
206
+ name: 'AbortError',
207
+ } );
208
+ } );
209
+
210
+ it( 'should return null if response has no content status code', async () => {
211
+ globalThis.fetch.mockResolvedValue( {
212
+ ok: true,
213
+ status: 204,
214
+ } );
215
+
216
+ await expect( apiFetch( { path: '/random' } ) ).resolves.toBe( null );
217
+ } );
218
+
219
+ it( 'should not try to parse the response', async () => {
220
+ const mockResponse = {
221
+ ok: true,
222
+ status: 200,
223
+ };
202
224
 
203
- try {
204
- await promise;
205
- } catch ( err ) {
206
- error = err;
207
- }
225
+ globalThis.fetch.mockResolvedValue( mockResponse );
208
226
 
209
- expect( error.name ).toBe( 'AbortError' );
227
+ await expect(
228
+ apiFetch( { path: '/random', parse: false } )
229
+ ).resolves.toBe( mockResponse );
210
230
  } );
211
231
 
212
- it( 'should return null if response has no content status code', () => {
213
- window.fetch.mockReturnValue(
214
- Promise.resolve( {
215
- status: 204,
216
- } )
217
- );
232
+ it( 'should not try to parse the error', async () => {
233
+ const mockResponse = {
234
+ ok: false,
235
+ status: 400,
236
+ };
218
237
 
219
- return apiFetch( { path: '/random' } ).catch( ( body ) => {
220
- // eslint-disable-next-line jest/no-conditional-expect
221
- expect( body ).toEqual( null );
222
- } );
238
+ globalThis.fetch.mockResolvedValue( mockResponse );
239
+
240
+ await expect(
241
+ apiFetch( { path: '/random', parse: false } )
242
+ ).rejects.toBe( mockResponse );
223
243
  } );
224
244
 
225
- it( 'should not try to parse the response', () => {
226
- window.fetch.mockReturnValue(
227
- Promise.resolve( {
228
- status: 200,
229
- } )
230
- );
245
+ it( 'should refetch after successful nonce refresh', async () => {
246
+ apiFetch.nonceMiddleware =
247
+ apiFetch.createNonceMiddleware( 'old-nonce' );
248
+ apiFetch.use( apiFetch.nonceMiddleware );
249
+ apiFetch.nonceEndpoint = '/rest-nonce';
250
+
251
+ globalThis.fetch.mockImplementation( async ( path, options ) => {
252
+ if ( path.startsWith( '/random' ) ) {
253
+ if ( options?.headers[ 'X-WP-Nonce' ] === 'new-nonce' ) {
254
+ return {
255
+ ok: true,
256
+ status: 200,
257
+ json: async () => ( { code: 'success' } ),
258
+ };
259
+ }
260
+
261
+ return {
262
+ ok: false,
263
+ status: 403,
264
+ json: async () => ( { code: 'rest_cookie_invalid_nonce' } ),
265
+ };
266
+ }
231
267
 
232
- return apiFetch( { path: '/random', parse: false } ).then(
233
- ( response ) => {
234
- expect( response ).toEqual( {
268
+ if ( path.startsWith( '/rest-nonce' ) ) {
269
+ return {
270
+ ok: true,
235
271
  status: 200,
236
- } );
272
+ text: async () => 'new-nonce',
273
+ };
237
274
  }
238
- );
275
+
276
+ return {
277
+ ok: false,
278
+ status: 404,
279
+ json: async () => ( { code: 'rest_no_route' } ),
280
+ };
281
+ } );
282
+
283
+ await expect( apiFetch( { path: '/random' } ) ).resolves.toEqual( {
284
+ code: 'success',
285
+ } );
239
286
  } );
240
287
 
241
- it( 'should not try to parse the error', () => {
242
- window.fetch.mockReturnValue(
243
- Promise.resolve( {
244
- status: 400,
245
- } )
246
- );
288
+ it( 'should fail with rest_cookie_invalid_nonce after failed nonce refresh', async () => {
289
+ apiFetch.nonceMiddleware =
290
+ apiFetch.createNonceMiddleware( 'old-nonce' );
291
+ apiFetch.use( apiFetch.nonceMiddleware );
292
+ apiFetch.nonceEndpoint = '/rest-nonce';
293
+
294
+ globalThis.fetch.mockImplementation( async ( path, options ) => {
295
+ if ( path.startsWith( '/random' ) ) {
296
+ if ( options?.headers[ 'X-WP-Nonce' ] === 'new-nonce' ) {
297
+ return {
298
+ ok: true,
299
+ status: 200,
300
+ json: async () => ( { code: 'success' } ),
301
+ };
302
+ }
303
+
304
+ return {
305
+ ok: false,
306
+ status: 403,
307
+ json: async () => ( { code: 'rest_cookie_invalid_nonce' } ),
308
+ };
309
+ }
247
310
 
248
- return apiFetch( { path: '/random', parse: false } ).catch(
249
- ( response ) => {
250
- // eslint-disable-next-line jest/no-conditional-expect
251
- expect( response ).toEqual( {
311
+ if ( path.startsWith( '/rest-nonce' ) ) {
312
+ return {
313
+ ok: false,
252
314
  status: 400,
253
- } );
315
+ text: async () => '0',
316
+ };
254
317
  }
255
- );
318
+
319
+ return {
320
+ ok: false,
321
+ status: 404,
322
+ json: async () => ( { code: 'rest_no_route' } ),
323
+ };
324
+ } );
325
+
326
+ await expect( apiFetch( { path: '/random' } ) ).rejects.toEqual( {
327
+ code: 'rest_cookie_invalid_nonce',
328
+ } );
256
329
  } );
257
330
 
258
- it( 'should not use the default fetch handler when using a custom fetch handler', () => {
331
+ it( 'should not use the default fetch handler when using a custom fetch handler', async () => {
259
332
  const customFetchHandler = jest.fn();
260
333
 
261
334
  apiFetch.setFetchHandler( customFetchHandler );
262
335
 
263
- apiFetch( { path: '/random' } );
336
+ await apiFetch( { path: '/random' } );
264
337
 
265
- expect( window.fetch ).not.toHaveBeenCalled();
338
+ expect( globalThis.fetch ).not.toHaveBeenCalled();
266
339
 
267
340
  expect( customFetchHandler ).toHaveBeenCalledWith( {
268
341
  path: '/random?_locale=user',
269
342
  } );
270
343
  } );
271
344
 
272
- it( 'should run the last-registered user-defined middleware first', () => {
273
- // This could potentially impact other tests in that a lingering
274
- // middleware is left. For the purposes of this test, it is sufficient
275
- // to ensure that the last-registered middleware receives the original
276
- // options object. It also assumes that some built-in middleware would
277
- // either mutate or clone the original options if the extra middleware
278
- // had been pushed to the stack.
279
- expect.assertions( 1 );
345
+ it( 'should run the last-registered user-defined middleware first', async () => {
346
+ // The test assumes that some built-in middleware will either mutate or clone
347
+ // the original options if the extra middleware had been pushed to the stack.
280
348
 
281
349
  const expectedOptions = {};
282
350
 
@@ -286,6 +354,9 @@ describe( 'apiFetch', () => {
286
354
  return next( actualOptions );
287
355
  } );
288
356
 
289
- apiFetch( expectedOptions );
357
+ // Set a custom fetch handler to avoid using the default fetch handler.
358
+ apiFetch.setFetchHandler( jest.fn() );
359
+
360
+ await apiFetch( expectedOptions );
290
361
  } );
291
362
  } );
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
- export interface APIFetchOptions extends RequestInit {
1
+ export interface APIFetchOptions< Parse extends boolean = boolean >
2
+ extends RequestInit {
2
3
  // Override headers, we only accept it as an object due to the `nonce` middleware
3
4
  headers?: Record< string, string >;
4
5
  path?: string;
@@ -6,13 +7,17 @@ export interface APIFetchOptions extends RequestInit {
6
7
  /**
7
8
  * @default true
8
9
  */
9
- parse?: boolean;
10
+ parse?: Parse;
10
11
  data?: any;
11
12
  namespace?: string;
12
13
  endpoint?: string;
13
14
  }
14
15
 
15
- export type APIFetchMiddleware = (
16
- options: APIFetchOptions,
17
- next: ( nextOptions: APIFetchOptions ) => Promise< any >
16
+ export type FetchHandler< Parse extends boolean = boolean > = (
17
+ nextOptions: APIFetchOptions< Parse >
18
+ ) => Promise< any >;
19
+
20
+ export type APIFetchMiddleware< Parse extends boolean = boolean > = (
21
+ options: APIFetchOptions< Parse >,
22
+ next: FetchHandler< Parse >
18
23
  ) => Promise< any >;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from '@wordpress/i18n';
5
+
6
+ /**
7
+ * Calls the `json` function on the Response, throwing an error if the response
8
+ * doesn't have a json function or if parsing the json itself fails.
9
+ *
10
+ * @param response
11
+ * @return Parsed response.
12
+ */
13
+ async function parseJsonAndNormalizeError( response: Response ) {
14
+ try {
15
+ return await response.json();
16
+ } catch {
17
+ throw {
18
+ code: 'invalid_json',
19
+ message: __( 'The response is not a valid JSON response.' ),
20
+ };
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Parses the apiFetch response properly and normalize response errors.
26
+ *
27
+ * @param response
28
+ * @param shouldParseResponse
29
+ *
30
+ * @return Parsed response.
31
+ */
32
+ export async function parseResponseAndNormalizeError(
33
+ response: Response,
34
+ shouldParseResponse = true
35
+ ) {
36
+ if ( ! shouldParseResponse ) {
37
+ return response;
38
+ }
39
+
40
+ if ( response.status === 204 ) {
41
+ return null;
42
+ }
43
+
44
+ return await parseJsonAndNormalizeError( response );
45
+ }
46
+
47
+ /**
48
+ * Parses a response, throwing an error if parsing the response fails.
49
+ *
50
+ * @param response
51
+ * @param shouldParseResponse
52
+ * @return Never returns, always throws.
53
+ */
54
+ export async function parseAndThrowError(
55
+ response: Response,
56
+ shouldParseResponse = true
57
+ ) {
58
+ if ( ! shouldParseResponse ) {
59
+ throw response;
60
+ }
61
+
62
+ // Parse the response JSON and throw it as an error.
63
+ throw await parseJsonAndNormalizeError( response );
64
+ }