fch 3.0.4 → 4.0.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/fetch.min.js +1 -1
- package/package.json +4 -6
- package/readme.md +193 -127
- package/fetch.js +0 -200
package/fetch.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const e=(e,{after:t,cache:r,error:o,output:a})=>fetch(e.url,e).then((async e=>{r&&r.clear();let s={status:e.status,statusText:e.statusText,headers:{}};for(let t of e.headers.keys())s.headers[t.toLowerCase()]=e.headers.get(t);if(!e.ok){const t=new Error(e.statusText);return t.response=s,o(t)}if(s=t(s),"stream"===a)return e.body;if(e[a]&&"function"==typeof e[a])return e[a]();const n=e.headers.get("content-type"),u=n&&n.includes("application/json");if(s.body=s.body||await(u?e.json():e.text()),"body"===a)return s.body;if("response"===a)return s;throw new Error(`Invalid option output="${a}"`)})),t=(r={})=>{const o=(()=>{const e=new Map;return t=>({save:r=>(e.set(t,r),r),get:()=>e.get(t),clear:()=>e.delete(t)})})(),a=async(t="/",r={})=>{"object"!=typeof r&&(r={}),r="string"==typeof t?{url:t,...r}:t;let{dedupe:s,output:n,before:u,after:d,error:c,...f}={...a,...r};f.url=((e,t,r)=>{let[o,a={}]=e.split("?");const s=new URLSearchParams({...Object.fromEntries(new URLSearchParams(t)),...Object.fromEntries(new URLSearchParams(a))}).toString();return s&&(o=o+"?"+s),r?new URL(o,r).href:o})(f.url,{...a.query,...r.query},f.baseUrl??f.baseURL),f.method=f.method.toLowerCase(),f.headers=(e=>{const t={};for(let[r,o]of Object.entries(e))t[r.toLowerCase()]=o;return t})({...a.headers,...r.headers});const p=!(!s||"get"!==f.method)&&o(f.url);var i;return"object"!=typeof(i=f.body)||i instanceof FormData||i.pipe||(f.body=JSON.stringify(f.body),f.headers["content-type"]="application/json"),f=u(f),p&&!f.signal?p.get()?p.get():p.save(e(f,{cache:p,output:n,error:c,after:d})):e(f,{output:n,error:c,after:d})};return a.url=r.url??"/",a.method=r.method??"get",a.query=r.query??{},a.headers=r.headers??{},a.baseUrl=r.baseUrl??r.baseURL??null,a.dedupe=r.dedupe??!0,a.output=r.output??"body",a.credentials=r.credentials??"include",a.before=r.before??(e=>e),a.after=r.after??(e=>e),a.error=r.error??(e=>{throw e}),["get","head","post","patch","put","delete"].map((e=>{a[e]=(t,r={})=>a(t,{...r,method:e})})),a.create=t,a};"undefined"!=typeof window&&(window.fch=t());export default t();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Fetch interface with better promises, deduplication, defaults, etc.",
|
|
5
5
|
"homepage": "https://github.com/franciscop/fetch",
|
|
6
6
|
"repository": "https://github.com/franciscop/fetch.git",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "terser --compress --mangle -- ./fetch.js
|
|
12
|
+
"build": "terser --compress --mangle --toplevel -o ./fetch.min.js -- ./fetch.js",
|
|
13
|
+
"size": "echo $(gzip -c fetch.min.js | wc -c) bytes",
|
|
13
14
|
"start": "npm run watch # Start ~= Start dev",
|
|
14
15
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles",
|
|
15
16
|
"watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles"
|
|
@@ -22,11 +23,8 @@
|
|
|
22
23
|
"async",
|
|
23
24
|
"ajax"
|
|
24
25
|
],
|
|
25
|
-
"main": "./fetch.js",
|
|
26
|
-
"module": "./fetch.js",
|
|
27
|
-
"browser": "./fetch.min.js",
|
|
26
|
+
"main": "./fetch.min.js",
|
|
28
27
|
"files": [
|
|
29
|
-
"fetch.js",
|
|
30
28
|
"fetch.min.js"
|
|
31
29
|
],
|
|
32
30
|
"type": "module",
|
package/readme.md
CHANGED
|
@@ -3,47 +3,53 @@
|
|
|
3
3
|
A tiny library to make API calls easier. Similar to Axios, but tiny size and simpler API:
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const mew = await
|
|
6
|
+
// Plain usage
|
|
7
|
+
import fch from "fch";
|
|
8
|
+
const mew = await fch("https://pokeapi.co/pokemon/150");
|
|
9
9
|
console.log(mew);
|
|
10
|
+
|
|
11
|
+
// As an API abstraction
|
|
12
|
+
const api = fch.create({ baseUrl: "https://pokeapi.co/" });
|
|
13
|
+
const mew = await api.get("/pokemon/150");
|
|
14
|
+
await api.patch("/pokemon/150", { body: { type: "psychic" } });
|
|
10
15
|
```
|
|
11
16
|
|
|
12
17
|
- Create instances with shared options across requests.
|
|
13
18
|
- Automatically encode object and array bodies as JSON.
|
|
14
19
|
- Automatically decode JSON responses based on the headers.
|
|
15
20
|
- Await/Async Promises; `>= 400 and <= 100` will _reject_ with an error.
|
|
21
|
+
- Credentials: "include" by default
|
|
16
22
|
- Interceptors: `before` the request, `after` the response and catch with `error`.
|
|
17
23
|
- Deduplicates parallel GET requests.
|
|
18
24
|
- Works the same way in Node.js and the browser.
|
|
19
25
|
- No dependencies; include it with a simple `<script>` on the browser.
|
|
20
26
|
|
|
21
27
|
```js
|
|
22
|
-
import api from
|
|
23
|
-
|
|
24
|
-
api.get(url, { headers, ...options })
|
|
25
|
-
api.head(url, { headers, ...options })
|
|
26
|
-
api.post(url, { body, headers, ...options })
|
|
27
|
-
api.patch(url, { body, headers, ...options })
|
|
28
|
-
api.put(url, { body, headers, ...options })
|
|
29
|
-
api.del(url, { body, headers, ...options })
|
|
30
|
-
|
|
31
|
-
api.create({ url, body, headers, ...options})
|
|
32
|
-
```
|
|
28
|
+
import api from "fch";
|
|
33
29
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
| [`
|
|
30
|
+
api.get(url, { headers, ...options });
|
|
31
|
+
api.head(url, { headers, ...options });
|
|
32
|
+
api.post(url, { body, headers, ...options });
|
|
33
|
+
api.patch(url, { body, headers, ...options });
|
|
34
|
+
api.put(url, { body, headers, ...options });
|
|
35
|
+
api.delete(url, { body, headers, ...options });
|
|
36
|
+
|
|
37
|
+
api.create({ url, body, headers, ...options });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Options | Default | Description |
|
|
41
|
+
| ------------------------- | ------------------ | ---------------------------------------- |
|
|
42
|
+
| [`method`](#method) | `"get"` | Default method to use for the call |
|
|
43
|
+
| [`url`](#url) | `"/"` | The path or url for the request |
|
|
44
|
+
| [`baseUrl`](#url) | `null` | The shared base of the API |
|
|
45
|
+
| [`body`](#body) | `null` | The body to send with the request |
|
|
46
|
+
| [`query`](#query) | `{}` | Add query parameters to the URL |
|
|
47
|
+
| [`headers`](#headers) | `{}` | Shared headers across all requests |
|
|
48
|
+
| [`output`](#output) | `"body"` | The return value of the API call |
|
|
49
|
+
| [`dedupe`](#dedupe) | `true` | Reuse concurrently GET requests |
|
|
50
|
+
| [`before`](#interceptors) | `req => req` | Process the request before sending it |
|
|
51
|
+
| [`after`](#interceptors) | `res => res` | Process the response before returning it |
|
|
52
|
+
| [`error`](#interceptors) | `err => throw err` | Process errors before returning them |
|
|
47
53
|
|
|
48
54
|
## Getting Started
|
|
49
55
|
|
|
@@ -56,17 +62,17 @@ npm install fch
|
|
|
56
62
|
Then import it to be able to use it in your code:
|
|
57
63
|
|
|
58
64
|
```js
|
|
59
|
-
import fch from
|
|
60
|
-
const body = await fch.get(
|
|
65
|
+
import fch from "fch";
|
|
66
|
+
const body = await fch.get("/");
|
|
61
67
|
```
|
|
62
68
|
|
|
63
69
|
On the browser you can add it with a script and it will be available as `fch`:
|
|
64
70
|
|
|
65
71
|
```html
|
|
66
72
|
<!-- Import it as usual -->
|
|
67
|
-
<script src="https://cdn.jsdelivr.net/npm/fch"></script>
|
|
68
|
-
<script>
|
|
69
|
-
fch(
|
|
73
|
+
<script src="https://cdn.jsdelivr.net/npm/fch" type="module"></script>
|
|
74
|
+
<script type="module">
|
|
75
|
+
fch("/hello");
|
|
70
76
|
</script>
|
|
71
77
|
```
|
|
72
78
|
|
|
@@ -75,23 +81,23 @@ On the browser you can add it with a script and it will be available as `fch`:
|
|
|
75
81
|
These are all available options and their defaults:
|
|
76
82
|
|
|
77
83
|
```js
|
|
78
|
-
import api from
|
|
84
|
+
import api from "fch";
|
|
79
85
|
|
|
80
86
|
// General options with their defaults; all of these are also parameters:
|
|
81
|
-
api.method =
|
|
82
|
-
api.url =
|
|
83
|
-
api.baseUrl = null;
|
|
84
|
-
api.query = {};
|
|
85
|
-
api.headers = {};
|
|
87
|
+
api.method = "get"; // Default method to use for api()
|
|
88
|
+
api.url = "/"; // Relative or absolute url where the request is sent
|
|
89
|
+
api.baseUrl = null; // Set an API base URL reused all across requests
|
|
90
|
+
api.query = {}; // Merged with the query parameters passed manually
|
|
91
|
+
api.headers = {}; // Merged with the headers on a per-request basis
|
|
86
92
|
|
|
87
93
|
// Control simple variables
|
|
88
|
-
api.output =
|
|
89
|
-
api.dedupe = true;
|
|
94
|
+
api.output = "body"; // Return the parsed body; use 'response' or 'stream' otherwise
|
|
95
|
+
api.dedupe = true; // Avoid sending concurrent GET requests to the same path
|
|
90
96
|
|
|
91
97
|
// Interceptors
|
|
92
|
-
api.before = req => req;
|
|
93
|
-
api.after = res => res;
|
|
94
|
-
api.error = err => Promise.reject(err);
|
|
98
|
+
api.before = (req) => req;
|
|
99
|
+
api.after = (res) => res;
|
|
100
|
+
api.error = (err) => Promise.reject(err);
|
|
95
101
|
```
|
|
96
102
|
|
|
97
103
|
They can all be defined globally as shown above, passed manually as the options argument, or be used when [creating a new instance](#create-an-instance).
|
|
@@ -101,11 +107,11 @@ They can all be defined globally as shown above, passed manually as the options
|
|
|
101
107
|
The HTTP method to make the request. When using the shorthand, it defaults to `GET`. We recommend using the method syntax:
|
|
102
108
|
|
|
103
109
|
```js
|
|
104
|
-
import api from
|
|
110
|
+
import api from "fch";
|
|
105
111
|
|
|
106
|
-
api.get(
|
|
107
|
-
api.post(
|
|
108
|
-
api.put(`/cats/3`, { body: { name:
|
|
112
|
+
api.get("/cats");
|
|
113
|
+
api.post("/cats", { body: { name: "snowball" } });
|
|
114
|
+
api.put(`/cats/3`, { body: { name: "snowball" } });
|
|
109
115
|
```
|
|
110
116
|
|
|
111
117
|
You can use it with the plain function as an option parameter. The methods are all lowercase but the option as a parameter is case insensitive; it can be either uppercase or lowercase:
|
|
@@ -125,12 +131,12 @@ api({ method; 'get'})
|
|
|
125
131
|
Example: adding a new cat and fixing a typo:
|
|
126
132
|
|
|
127
133
|
```js
|
|
128
|
-
import api from
|
|
134
|
+
import api from "fch";
|
|
129
135
|
|
|
130
|
-
const cats = await api.get(
|
|
136
|
+
const cats = await api.get("/cats");
|
|
131
137
|
console.log(cats);
|
|
132
|
-
const { id } = await api.post(
|
|
133
|
-
await api.put(`/cats/${id}`, { body: { name:
|
|
138
|
+
const { id } = await api.post("/cats", { body: { name: "snowbll" } });
|
|
139
|
+
await api.put(`/cats/${id}`, { body: { name: "snowball" } });
|
|
134
140
|
```
|
|
135
141
|
|
|
136
142
|
### Url
|
|
@@ -138,23 +144,23 @@ await api.put(`/cats/${id}`, { body: { name: 'snowball' }})
|
|
|
138
144
|
Specify where to send the request to. It's normally the first argument, though technically you can use both styles:
|
|
139
145
|
|
|
140
146
|
```js
|
|
141
|
-
import api from
|
|
147
|
+
import api from "fch";
|
|
142
148
|
|
|
143
149
|
// Recommended way of specifying the Url
|
|
144
|
-
await api.post(
|
|
150
|
+
await api.post("/hello", { body: "...", headers: {} });
|
|
145
151
|
|
|
146
152
|
// These are also valid if you prefer their style; we won't judge
|
|
147
|
-
await api(
|
|
148
|
-
await api({ url:
|
|
149
|
-
await api.post({ url:
|
|
153
|
+
await api("/hello", { method: "post", body: "...", headers: {} });
|
|
154
|
+
await api({ url: "/hello", method: "post", headers: {}, body: "..." });
|
|
155
|
+
await api.post({ url: "/hello", headers: {}, body: "..." });
|
|
150
156
|
```
|
|
151
157
|
|
|
152
158
|
It can be either absolute or relative, in which case it'll use the local one in the page. It's recommending to set `baseUrl`:
|
|
153
159
|
|
|
154
160
|
```js
|
|
155
|
-
import api from
|
|
156
|
-
api.baseUrl =
|
|
157
|
-
api.get(
|
|
161
|
+
import api from "fch";
|
|
162
|
+
api.baseUrl = "https//api.filemon.io/";
|
|
163
|
+
api.get("/hello");
|
|
158
164
|
// Called https//api.filemon.io/hello
|
|
159
165
|
```
|
|
160
166
|
|
|
@@ -183,8 +189,10 @@ The methods `GET` and `HEAD` do not accept a body and it'll be ignored.
|
|
|
183
189
|
|
|
184
190
|
The **response body** will be returned by default as the output of the call:
|
|
185
191
|
|
|
192
|
+
> See more info in [**Output**](#output)
|
|
193
|
+
|
|
186
194
|
```js
|
|
187
|
-
const body = await api.get(
|
|
195
|
+
const body = await api.get("/cats");
|
|
188
196
|
console.log(body);
|
|
189
197
|
// [{ id: 1, }, ...]
|
|
190
198
|
```
|
|
@@ -194,51 +202,48 @@ When the server specifies the header `Content-Type` as `application/json`, then
|
|
|
194
202
|
When the function returns the response (if you set `output: "response"` as an option), then the body can be accessed as `response.body`:
|
|
195
203
|
|
|
196
204
|
```js
|
|
197
|
-
const response = await api.get(
|
|
205
|
+
const response = await api.get("/cats", { output: "response" });
|
|
198
206
|
console.log(response.body);
|
|
199
207
|
// [{ id: 1, }, ...]
|
|
200
208
|
```
|
|
201
209
|
|
|
202
|
-
|
|
203
210
|
### Query
|
|
204
211
|
|
|
205
212
|
You can easily pass GET query parameters by using the option `query`:
|
|
206
213
|
|
|
207
214
|
```js
|
|
208
|
-
api.get(
|
|
215
|
+
api.get("/cats", { query: { limit: 3 } });
|
|
209
216
|
// /cats?limit=3
|
|
210
217
|
```
|
|
211
218
|
|
|
212
219
|
While rare, some times you might want to persist a query parameter across requests and always include it; in that case, you can define it globally and it'll be added to every request:
|
|
213
220
|
|
|
214
221
|
```js
|
|
215
|
-
import api from
|
|
216
|
-
api.query.myparam =
|
|
222
|
+
import api from "fch";
|
|
223
|
+
api.query.myparam = "abc";
|
|
217
224
|
|
|
218
|
-
api.get(
|
|
225
|
+
api.get("/cats", { query: { limit: 3 } });
|
|
219
226
|
// /cats?limit=3&myparam=abc
|
|
220
227
|
```
|
|
221
228
|
|
|
222
|
-
|
|
223
229
|
### Headers
|
|
224
230
|
|
|
225
231
|
You can define headers globally, in which case they'll be added to every request, or locally, so that they are only added to the current request. You can also add them in the `before` callback:
|
|
226
232
|
|
|
227
|
-
|
|
228
233
|
```js
|
|
229
|
-
import api from
|
|
234
|
+
import api from "fch";
|
|
230
235
|
|
|
231
236
|
// Globally, so they are reused across all requests
|
|
232
|
-
api.headers.a =
|
|
237
|
+
api.headers.a = "b";
|
|
233
238
|
|
|
234
239
|
// With an interceptor, in case you need dynamic headers per-request
|
|
235
|
-
api.before = req => {
|
|
236
|
-
req.headers.c =
|
|
240
|
+
api.before = (req) => {
|
|
241
|
+
req.headers.c = "d";
|
|
237
242
|
return req;
|
|
238
243
|
};
|
|
239
244
|
|
|
240
245
|
// Set them for this single request:
|
|
241
|
-
api.get(
|
|
246
|
+
api.get("/hello", { headers: { e: "f" } });
|
|
242
247
|
// Total headers on the request:
|
|
243
248
|
// { a: 'b', c: 'd', e: 'f' }
|
|
244
249
|
```
|
|
@@ -249,31 +254,49 @@ When to use each?
|
|
|
249
254
|
- When you need to extract them dynamically from somewhere it's better to use the .before() interceptor. An example would be the user Authorization token.
|
|
250
255
|
- When it changes on each request, it's not consistent or it's an one-off, use the option argument.
|
|
251
256
|
|
|
252
|
-
|
|
253
257
|
### Output
|
|
254
258
|
|
|
255
259
|
This controls whether the call returns just the body (default), or the whole response. It can be controlled globally or on a per-request basis:
|
|
256
260
|
|
|
257
261
|
```js
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// "body" (default) or "response"
|
|
261
|
-
api.output = 'body';
|
|
262
|
-
|
|
263
|
-
// Return only the body, this is the default
|
|
264
|
-
const body = await api.get('/data');
|
|
262
|
+
// Return only the body (default)
|
|
263
|
+
const body = await api.get("/data");
|
|
265
264
|
|
|
266
265
|
// Return the whole response (with .body):
|
|
267
|
-
const response = await api.get(
|
|
266
|
+
const response = await api.get("/data", { output: "response" });
|
|
267
|
+
|
|
268
|
+
// Return a plain body stream
|
|
269
|
+
const stream = await api.get("/data", { output: "stream" });
|
|
270
|
+
stream.pipeTo(outStream);
|
|
268
271
|
|
|
269
|
-
//
|
|
270
|
-
const
|
|
272
|
+
// Return a blob, since `response.blob()` is available:
|
|
273
|
+
const blob = await api.get("/data", { output: "blob" });
|
|
271
274
|
```
|
|
272
275
|
|
|
276
|
+
There are few options that can be specified:
|
|
277
|
+
|
|
278
|
+
- `output: "body"` (default): returns the body, parsed as JSON or plain TEXT depending on the headers.
|
|
279
|
+
- `output: "response"`: return the full response with the properties `body`, `headers`, `status`, etc. The body will be parsed as JSON or plain TEXT depending on the headers. If you want the raw `response`, use `raw` or `clone` instead (see below in "raw" or "METHODS").
|
|
280
|
+
- `output: "stream"`: return a [web ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) of the body as the result of the promise.
|
|
281
|
+
- Any of [the valid methods](https://developer.mozilla.org/en-US/docs/Web/API/Response#methods):
|
|
282
|
+
- `output: "arrayBuffer"`: returns an arrayBuffer of the response body.
|
|
283
|
+
- `output: "blob"`: returns an arrayBuffer of the response body.
|
|
284
|
+
- `output: "clone"`: returns the raw Response, with the raw body. See also `raw` below.
|
|
285
|
+
- `output: formData`: returns an instance of FormData with all the parsed data.
|
|
286
|
+
- `output: json`: attempts to parse the response as JSON.
|
|
287
|
+
- `output: text`: puts the response body as plain text.
|
|
288
|
+
- `output: "raw"`: an alias for `clone`, returning the raw response (after passing through `after`).
|
|
289
|
+
|
|
290
|
+
For example, return the raw body as a `ReadableStream` with the option `stream`:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
const stream = await api.get('/cats', { output: 'stream' });
|
|
294
|
+
stream.pipeTo(...);
|
|
295
|
+
```
|
|
273
296
|
|
|
274
297
|
### Dedupe
|
|
275
298
|
|
|
276
|
-
When you perform a GET request to a given URL, but another GET request
|
|
299
|
+
When you perform a GET request to a given URL, but another GET request _to the same_ URL is ongoing, it'll **not** create a new request and instead reuse the response when the first one is finished:
|
|
277
300
|
|
|
278
301
|
```js
|
|
279
302
|
fetch.mockOnce("a").mockOnce("b");
|
|
@@ -287,12 +310,12 @@ You can disable this by setting either the global `fch.dedupe` option to `false`
|
|
|
287
310
|
|
|
288
311
|
```js
|
|
289
312
|
// Globally set it for all calls
|
|
290
|
-
fch.dedupe = true;
|
|
313
|
+
fch.dedupe = true; // [DEFAULT] Dedupes GET requests
|
|
291
314
|
fch.dedupe = false; // All fetch() calls trigger a network call
|
|
292
315
|
|
|
293
316
|
// Set it on a per-call basis
|
|
294
|
-
fch(
|
|
295
|
-
fch(
|
|
317
|
+
fch("/a", { dedupe: true }); // [DEFAULT] Dedupes GET requests
|
|
318
|
+
fch("/a", { dedupe: false }); // All fetch() calls trigger a network call
|
|
296
319
|
```
|
|
297
320
|
|
|
298
321
|
> We do not support deduping other methods besides `GET` right now
|
|
@@ -305,7 +328,7 @@ it("can opt out locally", async () => {
|
|
|
305
328
|
const res = await Promise.all([
|
|
306
329
|
fch("/a"),
|
|
307
330
|
fch("/a", { dedupe: false }),
|
|
308
|
-
fch("/a"),
|
|
331
|
+
fch("/a"), // Reuses the 1st response, not the 2nd one
|
|
309
332
|
]);
|
|
310
333
|
|
|
311
334
|
expect(res).toEqual(["a", "b", "a"]);
|
|
@@ -317,6 +340,12 @@ it("can opt out locally", async () => {
|
|
|
317
340
|
|
|
318
341
|
You can also easily add the interceptors `before`, `after` and `error`:
|
|
319
342
|
|
|
343
|
+
- `before`: Called when the request is fully formed, but before actually launching it.
|
|
344
|
+
- `before`: Called just after the response is created and if there was no error, but before parsing anything else.
|
|
345
|
+
- `error`: When the response is not okay, if possible it'll include the `response` object.
|
|
346
|
+
|
|
347
|
+
> Note: interceptors are never deduped/cached and always execute once per call, even if the main async fetch() has been deduped.
|
|
348
|
+
|
|
320
349
|
```js
|
|
321
350
|
// Perform an action or request transformation before the request is sent
|
|
322
351
|
fch.before = async req => {
|
|
@@ -346,7 +375,6 @@ fch.error = async err => {
|
|
|
346
375
|
};
|
|
347
376
|
```
|
|
348
377
|
|
|
349
|
-
|
|
350
378
|
## How to
|
|
351
379
|
|
|
352
380
|
### Stop errors from throwing
|
|
@@ -354,10 +382,11 @@ fch.error = async err => {
|
|
|
354
382
|
While you can handle this on a per-request basis, if you want to overwrite the global behavior you can write a interceptor:
|
|
355
383
|
|
|
356
384
|
```js
|
|
357
|
-
import fch from
|
|
358
|
-
fch.
|
|
385
|
+
import fch from "fch";
|
|
386
|
+
fch.output = "response";
|
|
387
|
+
fch.error = (error) => error.response;
|
|
359
388
|
|
|
360
|
-
const res = await fch(
|
|
389
|
+
const res = await fch("/notfound");
|
|
361
390
|
expect(res.status).toBe(404);
|
|
362
391
|
```
|
|
363
392
|
|
|
@@ -366,75 +395,83 @@ expect(res.status).toBe(404);
|
|
|
366
395
|
By default a successful request will just return the data. However this one is configurable on a global level:
|
|
367
396
|
|
|
368
397
|
```js
|
|
369
|
-
import fch from
|
|
370
|
-
fch.output =
|
|
398
|
+
import fch from "fch";
|
|
399
|
+
fch.output = "response";
|
|
371
400
|
|
|
372
|
-
const res = await fch(
|
|
401
|
+
const res = await fch("/hello");
|
|
402
|
+
console.log(res.status);
|
|
373
403
|
```
|
|
374
404
|
|
|
375
405
|
Or on a per-request level:
|
|
376
406
|
|
|
377
407
|
```js
|
|
378
|
-
import fch from
|
|
408
|
+
import fch from "fch";
|
|
379
409
|
|
|
380
410
|
// Valid values are 'body' (default) or 'response'
|
|
381
|
-
const res = await fch(
|
|
411
|
+
const res = await fch("/hello", { output: "response" });
|
|
382
412
|
|
|
383
413
|
// Does not affect others
|
|
384
|
-
const body = await fch(
|
|
414
|
+
const body = await fch("/hello");
|
|
385
415
|
```
|
|
386
416
|
|
|
417
|
+
It does perform some basic parsing of the `body`, if you don't want any of that you can retrieve the very raw response:
|
|
418
|
+
|
|
419
|
+
```js
|
|
420
|
+
import fch from "fch";
|
|
421
|
+
|
|
422
|
+
// Valid values are 'body' (default) or 'response'
|
|
423
|
+
const res = await fch("/hello", { output: "raw" });
|
|
424
|
+
console.log(res.body); // ReadableStream
|
|
425
|
+
```
|
|
387
426
|
|
|
388
427
|
### Set a base URL
|
|
389
428
|
|
|
390
429
|
There's a configuration parameter for that:
|
|
391
430
|
|
|
392
431
|
```js
|
|
393
|
-
import fch from
|
|
394
|
-
fch.baseUrl =
|
|
432
|
+
import fch from "fch";
|
|
433
|
+
fch.baseUrl = "https://api.filemon.io/";
|
|
395
434
|
|
|
396
435
|
// Calls "https://api.filemon.io/blabla"
|
|
397
|
-
const body = await fch.get(
|
|
436
|
+
const body = await fch.get("/blabla");
|
|
398
437
|
```
|
|
399
438
|
|
|
400
|
-
|
|
401
439
|
### Set authorization headers
|
|
402
440
|
|
|
403
441
|
You can set that globally as a header:
|
|
404
442
|
|
|
405
443
|
```js
|
|
406
|
-
import fch from
|
|
407
|
-
fch.headers.Authorization =
|
|
444
|
+
import fch from "fch";
|
|
445
|
+
fch.headers.Authorization = "bearer abc";
|
|
408
446
|
|
|
409
|
-
const me = await fch(
|
|
447
|
+
const me = await fch("/users/me");
|
|
410
448
|
```
|
|
411
449
|
|
|
412
450
|
Or globally on a per-request basis, for example if you take the value from localStorage:
|
|
413
451
|
|
|
414
452
|
```js
|
|
415
|
-
import fch from
|
|
453
|
+
import fch from "fch";
|
|
416
454
|
|
|
417
455
|
// All the requests will add the Authorization header when the token is
|
|
418
456
|
// in localStorage
|
|
419
|
-
fch.before = req => {
|
|
420
|
-
if (localStorage.get(
|
|
421
|
-
req.headers.Authorization =
|
|
457
|
+
fch.before = (req) => {
|
|
458
|
+
if (localStorage.get("token")) {
|
|
459
|
+
req.headers.Authorization = "bearer " + localStorage.get("token");
|
|
422
460
|
}
|
|
423
461
|
return req;
|
|
424
462
|
};
|
|
425
463
|
|
|
426
|
-
const me = await fch(
|
|
464
|
+
const me = await fch("/users/me");
|
|
427
465
|
```
|
|
428
466
|
|
|
429
467
|
Or on a per-request basis, though we wouldn't recommend this:
|
|
430
468
|
|
|
431
469
|
```js
|
|
432
|
-
import fch from
|
|
470
|
+
import fch from "fch";
|
|
433
471
|
|
|
434
|
-
const me = await fch(
|
|
472
|
+
const me = await fch("/users/me", { headers: { Authorization: "bearer abc" } });
|
|
435
473
|
```
|
|
436
474
|
|
|
437
|
-
|
|
438
475
|
### Create an instance
|
|
439
476
|
|
|
440
477
|
You can create an instance with its own defaults and global options easily. It's common when writing an API that you want to encapsulate away:
|
|
@@ -453,28 +490,47 @@ fch.get('/hello'); // Gets http://localhost:3000/hello (or wherever you are)
|
|
|
453
490
|
|
|
454
491
|
Note: for server-side (Node.js) usage, you always want to set `baseUrl`.
|
|
455
492
|
|
|
493
|
+
### Streaming a response body
|
|
494
|
+
|
|
495
|
+
To stream the body, you need to use the `output: "stream"` option so that it returns a [WebStream ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
|
|
496
|
+
|
|
497
|
+
```js
|
|
498
|
+
import fch from "fch";
|
|
499
|
+
|
|
500
|
+
// Valid values are 'body' (default) or 'response'
|
|
501
|
+
const stream = await fch("/data", { output: "stream" });
|
|
502
|
+
stream.pipeTo(outStream);
|
|
503
|
+
```
|
|
456
504
|
|
|
505
|
+
You might want to convert it to a Node.js ReadStream:
|
|
506
|
+
|
|
507
|
+
```js
|
|
508
|
+
import fch from "fch";
|
|
509
|
+
import { Readable } from "node:stream";
|
|
510
|
+
|
|
511
|
+
const stream = await fch("/data", { output: "stream" });
|
|
512
|
+
const readableNodeStream = Readable.fromWeb(stream);
|
|
513
|
+
// ...
|
|
514
|
+
```
|
|
457
515
|
|
|
458
516
|
### Cancel ongoing requests
|
|
459
517
|
|
|
460
518
|
You can cancel ongoing requests [similarly to native fetch()](https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort#examples), by passing it a signal:
|
|
461
519
|
|
|
462
520
|
```js
|
|
463
|
-
import api from
|
|
521
|
+
import api from "fch";
|
|
464
522
|
|
|
465
523
|
const controller = new AbortController();
|
|
466
524
|
const signal = controller.signal;
|
|
467
525
|
|
|
468
|
-
abortButton.addEventListener(
|
|
526
|
+
abortButton.addEventListener("click", () => {
|
|
469
527
|
controller.abort();
|
|
470
|
-
console.log(
|
|
528
|
+
console.log("Download aborted");
|
|
471
529
|
});
|
|
472
530
|
|
|
473
531
|
api.get(url, { signal });
|
|
474
532
|
```
|
|
475
533
|
|
|
476
|
-
|
|
477
|
-
|
|
478
534
|
### Define shared options
|
|
479
535
|
|
|
480
536
|
You can also define values straight away:
|
|
@@ -490,28 +546,24 @@ console.log(mew);
|
|
|
490
546
|
|
|
491
547
|
You can also [create an instance](#create-an-instance) that will have the same options for all requests made with that instance.
|
|
492
548
|
|
|
493
|
-
|
|
494
|
-
|
|
495
549
|
### Node.js vs Browser
|
|
496
550
|
|
|
497
551
|
First, we use the native Node.js' fetch() and the browser's native fetch(), so any difference between those also applies to this library. For example, if you were to call `"/"` in the browser it'd refer to the current URL, while in Node.js it'd fail since you need to specify the full URL. Some other places where you might find differences: CORS, cache, etc.
|
|
498
552
|
|
|
499
553
|
In the library itself there's nothing different between the browser and Node.js, but it might be interesting to note that (if/when implemented) things like cache, etc. in Node.js are normally long-lived and shared, while in a browser request it'd bound to the request itself.
|
|
500
554
|
|
|
501
|
-
|
|
502
|
-
|
|
503
555
|
### Differences with Axios
|
|
504
556
|
|
|
505
557
|
The main difference is that things are simplified with fch:
|
|
506
558
|
|
|
507
559
|
```js
|
|
508
560
|
// Modify headers
|
|
509
|
-
axios.defaults.headers.Authorization =
|
|
510
|
-
fch.headers.Authorization =
|
|
561
|
+
axios.defaults.headers.Authorization = "...";
|
|
562
|
+
fch.headers.Authorization = "...";
|
|
511
563
|
|
|
512
564
|
// Set a base URL
|
|
513
|
-
axios.defaults.baseURL =
|
|
514
|
-
fch.baseUrl =
|
|
565
|
+
axios.defaults.baseURL = "...";
|
|
566
|
+
fch.baseUrl = "...";
|
|
515
567
|
|
|
516
568
|
// Add an interceptor
|
|
517
569
|
axios.interceptors.request.use(fn);
|
|
@@ -524,3 +576,17 @@ As disadvantages, I can think of two major ones for `fch`:
|
|
|
524
576
|
|
|
525
577
|
- Requires Node.js 18+, which is the version that includes `fetch()` by default.
|
|
526
578
|
- Does not support many of the more advanced options, like `onUploadProgress` nor `onDownloadProgress`.
|
|
579
|
+
|
|
580
|
+
## Releases
|
|
581
|
+
|
|
582
|
+
### V4
|
|
583
|
+
|
|
584
|
+
Breaking changes:
|
|
585
|
+
|
|
586
|
+
- Only ESM exports. Meaning, if you use it in a browser you'll need the `<script type="module">`.
|
|
587
|
+
- The method `fch.del()` (and derivates with fch.create()) have been renamed to `fch.delete()`.
|
|
588
|
+
|
|
589
|
+
Changes:
|
|
590
|
+
|
|
591
|
+
- Added `output` options: `raw`, `stream`, `arrayBuffer`, `blob`, `clone`, `formData`, `json`, `text`
|
|
592
|
+
- Gone from 1.2kb down to 1.0kb
|
package/fetch.js
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
// Plain-ish object
|
|
2
|
-
const hasPlainBody = (options) => {
|
|
3
|
-
if (options.headers["content-type"]) return;
|
|
4
|
-
if (!["object", "array"].includes(typeof options.body)) return;
|
|
5
|
-
if (options.body instanceof FormData) return;
|
|
6
|
-
return true;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const createUrl = (url, query, base) => {
|
|
10
|
-
let [path, urlQuery = {}] = url.split("?");
|
|
11
|
-
|
|
12
|
-
// Merge global params with passed params with url params
|
|
13
|
-
const entries = new URLSearchParams({
|
|
14
|
-
...Object.fromEntries(new URLSearchParams(query)),
|
|
15
|
-
...Object.fromEntries(new URLSearchParams(urlQuery)),
|
|
16
|
-
}).toString();
|
|
17
|
-
if (entries) {
|
|
18
|
-
path = path + "?" + entries;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (!base) return path;
|
|
22
|
-
const fullUrl = new URL(path, base);
|
|
23
|
-
return fullUrl.href;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const createHeaders = (user, base) => {
|
|
27
|
-
// User-set headers overwrite the base headers
|
|
28
|
-
const headers = { ...base, ...user };
|
|
29
|
-
|
|
30
|
-
// Make the headers lowercase
|
|
31
|
-
for (let key in headers) {
|
|
32
|
-
const value = headers[key];
|
|
33
|
-
delete headers[key];
|
|
34
|
-
headers[key.toLowerCase()] = value;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return headers;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const createDedupe = (ongoing, url) => ({
|
|
41
|
-
save: (prom) => {
|
|
42
|
-
ongoing.set(url, prom);
|
|
43
|
-
return prom;
|
|
44
|
-
},
|
|
45
|
-
get: () => ongoing.get(url),
|
|
46
|
-
clear: () => ongoing.delete(url),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const createFetch = (request, { after, dedupe, error, output }) => {
|
|
50
|
-
return fetch(request.url, request).then(async (res) => {
|
|
51
|
-
// No longer ongoing at this point
|
|
52
|
-
if (dedupe) dedupe.clear();
|
|
53
|
-
|
|
54
|
-
// Need to manually create it to set some things like the proper response
|
|
55
|
-
let response = {
|
|
56
|
-
status: res.status,
|
|
57
|
-
statusText: res.statusText,
|
|
58
|
-
headers: {},
|
|
59
|
-
};
|
|
60
|
-
for (let key of res.headers.keys()) {
|
|
61
|
-
response.headers[key.toLowerCase()] = res.headers.get(key);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Oops, throw it
|
|
65
|
-
if (!res.ok) {
|
|
66
|
-
const err = new Error(res.statusText);
|
|
67
|
-
err.response = response;
|
|
68
|
-
return error(err);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Automatically parse the response
|
|
72
|
-
const isJson = res.headers.get("content-type").includes("application/json");
|
|
73
|
-
response.body = await (isJson ? res.json() : res.text());
|
|
74
|
-
|
|
75
|
-
// Hijack the response and modify it
|
|
76
|
-
if (after) {
|
|
77
|
-
response = after(response);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (output === "body") {
|
|
81
|
-
return response.body;
|
|
82
|
-
} else {
|
|
83
|
-
return response;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const create = (defaults = {}) => {
|
|
89
|
-
// DEDUPLICATION is created on a per-instance basis
|
|
90
|
-
// To avoid making parallel requests to the same url if one is ongoing
|
|
91
|
-
const ongoing = new Map();
|
|
92
|
-
|
|
93
|
-
const fch = async (url, options = {}) => {
|
|
94
|
-
// Second parameter always has to be an object, even when it defaults
|
|
95
|
-
if (typeof options !== "object") options = {};
|
|
96
|
-
|
|
97
|
-
// Accept either fch(options) or fch(url, options)
|
|
98
|
-
options = typeof url === "string" ? { url, ...options } : url || {};
|
|
99
|
-
|
|
100
|
-
// Exctract the options
|
|
101
|
-
let {
|
|
102
|
-
dedupe = fch.dedupe,
|
|
103
|
-
output = fch.output,
|
|
104
|
-
baseURL = fch.baseURL, // DO NOT USE; it's here only for user friendliness
|
|
105
|
-
baseUrl = baseURL || fch.baseUrl,
|
|
106
|
-
|
|
107
|
-
// Extract it since it should not be part of fetch()
|
|
108
|
-
query = {},
|
|
109
|
-
|
|
110
|
-
// Interceptors can also be passed as parameters
|
|
111
|
-
before = fch.before,
|
|
112
|
-
after = fch.after,
|
|
113
|
-
error = fch.error,
|
|
114
|
-
|
|
115
|
-
...request
|
|
116
|
-
} = options; // Local option OR global value (including defaults)
|
|
117
|
-
|
|
118
|
-
// Merge it, first the global and then the local
|
|
119
|
-
query = { ...fch.query, ...query };
|
|
120
|
-
// Absolute URL if possible; Default method; merge the default headers
|
|
121
|
-
request.url = createUrl(request.url ?? fch.url, query, baseUrl);
|
|
122
|
-
request.method = (request.method ?? fch.method).toLowerCase();
|
|
123
|
-
request.headers = createHeaders(request.headers, fch.headers);
|
|
124
|
-
|
|
125
|
-
if (request.method !== "get") {
|
|
126
|
-
dedupe = false;
|
|
127
|
-
}
|
|
128
|
-
if (dedupe) {
|
|
129
|
-
dedupe = createDedupe(ongoing, request.url);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (!["body", "response"].includes(output)) {
|
|
133
|
-
const msg = `options.output needs to be either "body" (default) or "response", not "${output}"`;
|
|
134
|
-
throw new Error(msg);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// JSON-encode plain objects
|
|
138
|
-
if (hasPlainBody(request)) {
|
|
139
|
-
request.body = JSON.stringify(request.body);
|
|
140
|
-
request.headers["content-type"] = "application/json; charset=utf-8";
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Hijack the requeset and modify it
|
|
144
|
-
if (before) {
|
|
145
|
-
request = before(request);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// It should be cached and it's not being manually manipulated
|
|
149
|
-
if (dedupe && !request.signal) {
|
|
150
|
-
// It's already cached! Just return it
|
|
151
|
-
if (dedupe.get()) return dedupe.get();
|
|
152
|
-
|
|
153
|
-
// Otherwise, save it in the cache and return the promise
|
|
154
|
-
return dedupe.save(
|
|
155
|
-
createFetch(request, { dedupe, output, error, after })
|
|
156
|
-
);
|
|
157
|
-
} else {
|
|
158
|
-
// PUT, POST, etc should never dedupe and just return the plain request
|
|
159
|
-
return createFetch(request, { output, error, after });
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
// Default values
|
|
164
|
-
fch.url = defaults.url ?? "/";
|
|
165
|
-
fch.method = defaults.method ?? "get";
|
|
166
|
-
fch.query = defaults.query ?? {};
|
|
167
|
-
fch.headers = defaults.headers ?? {};
|
|
168
|
-
|
|
169
|
-
// Default options
|
|
170
|
-
fch.dedupe = defaults.dedupe ?? true;
|
|
171
|
-
fch.output = defaults.output ?? "body";
|
|
172
|
-
fch.credentials = defaults.credentials ?? "include";
|
|
173
|
-
|
|
174
|
-
// Interceptors
|
|
175
|
-
fch.before = defaults.before ?? ((request) => request);
|
|
176
|
-
fch.after = defaults.after ?? ((response) => response);
|
|
177
|
-
fch.error = defaults.error ?? ((error) => Promise.reject(error));
|
|
178
|
-
|
|
179
|
-
const get = (url, opts = {}) => fch(url, { ...opts });
|
|
180
|
-
const head = (url, opts = {}) => fch(url, { ...opts, method: "head" });
|
|
181
|
-
const post = (url, opts = {}) => fch(url, { ...opts, method: "post" });
|
|
182
|
-
const patch = (url, opts = {}) => fch(url, { ...opts, method: "patch" });
|
|
183
|
-
const put = (url, opts = {}) => fch(url, { ...opts, method: "put" });
|
|
184
|
-
const del = (url, opts = {}) => fch(url, { ...opts, method: "delete" });
|
|
185
|
-
|
|
186
|
-
fch.get = get;
|
|
187
|
-
fch.head = head;
|
|
188
|
-
fch.post = post;
|
|
189
|
-
fch.patch = patch;
|
|
190
|
-
fch.put = put;
|
|
191
|
-
fch.del = del;
|
|
192
|
-
|
|
193
|
-
fch.create = create;
|
|
194
|
-
|
|
195
|
-
return fch;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const fch = create();
|
|
199
|
-
|
|
200
|
-
export default fch;
|