UNPKG

12.8 kBMarkdownView Raw
1# Flighty ✈️
2
3[![NPM Version](https://img.shields.io/npm/v/flighty.svg?branch=master)](https://www.npmjs.com/package/flighty)
4[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5[![dependencies Status](https://david-dm.org/akmjenkins/flighty/status.svg)](https://david-dm.org/akmjenkins/flighty)
6[![codecov](https://codecov.io/gh/akmjenkins/flighty/branch/master/graph/badge.svg)](https://codecov.io/gh/akmjenkins/flighty)
7[![Build Status](https://travis-ci.org/akmjenkins/flighty.svg?branch=master)](https://travis-ci.org/akmjenkins/flighty)
8
9Simple (and tiny) fetch wrapper with nifty features such as intercepts, ***easy*** aborts, and retries, for everywhere.
10
11## Motivation
12
13Yet another fetch wrapping library? Well, various fetch-wrapping libraries have some of the above features, but few (if any) have them all.
14
15More importantly, almost all fetch wrapping libraries investigated include their polyfills right in the main packages (or don't include polyfill's at all requiring you to find out what you're missing). Flighty has an opt-in polyfill for [fetch](https://www.npmjs.com/package/cross-fetch) (and tiny polyfills for [AbortController](https://www.npmjs.com/package/abortcontroller-polyfill) and [ES6 promise](https://github.com/taylorhakes/promise-polyfill), because you'll likely need those, too if you don't have fetch), so you don't have to bloat your code if you don't absolutely need to.
16
17So, Flighty is BYOF - bring your own fetch - if you use it as is, but you can always opt-in to a fetch-polyfill if you aren't sure what environment your code will ultimately be running in:
18
19```js
20// without polyfill
21import Flighty from "flighty";
22
23// with polyfill
24import Flighty from "flighty/fetch";
25```
26
27Boom, now you've got a full featured fetch-wrapping library everywhere.
28
29If you're using Flighty as a standalone library in the browser, you can relax, it weighs just **5kb** minified and gzipped and less than 9kb if you're supporting old-IE and want to include a fetch (and abortcontroller and promise) polyfill.
30
31## Use it in unit testing
32
33Keep it short - don't mock Flighty. It'd be over-complicated and unnecessary to mock it's features - so just mock the fetch and let Flighty do it's thing in your tests. I recommend [jest-fetch-mock](https://www.npmjs.com/package/jest-fetch-mock).
34
35## Features
36
37**Drop in replacement** for fetch. This works:
38
39```js
40const res = await fetch('/somepath',{some options});
41
42const api = new Flighty({some options});
43const res = await api.get('/somepath',{some options});
44```
45
46This works because Flighty returns the standard [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) but with the addition of the **flighty object**.
47
48The drop in replacement makes this library relatively simple to add/remove from your codebase. If you keep your use of the flighty object on the response limited to interceptors and refactoring Flighty into/out of your codebase becomes a breeze.
49
50### flighty object
51
52```js
53res.flighty = {
54 method, // the method you called flighty with - e.g. get, post, put
55 retryCount, // the number of times this request has been retried
56 json, // what you'd normally get from await res.json()
57 text, // what you'd normally get from await res.text()
58 // the values the original flighty request was called with
59 call: {
60 path,
61 options,
62 extra
63 },
64 // retry method - calls the same request you made the first time again - hello JWT 401s
65 retry: async ()
66}
67
68```
69
70### Abort
71
72Flighty comes with two abort APIs. `abortAll()` which cancels all ongoing requests and cancellation via an `abortToken` (similar to [axios cancellation token](https://github.com/axios/axios#cancellation) but easier!).
73
74Aborting Fetch requests comes with a hairy, verbose [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) API that requires you to construct, pass in a signal to the fetch, and then abort the controller like so:
75
76```js
77 const controller = new AbortController();
78 const req = fetch('/',{controller.signal})
79
80 controller.abort();
81
82 try {
83 const res = await req;
84 } catch(err) {
85 // AbortError!
86 }
87
88```
89
90Flighty allows you to pass in a token (any Symbol) and then call `abort(token)` to cancel the request.
91
92```js
93 const req = flighty.get('/',{abortToken:"my token"});
94 api.abort("my token");
95
96 try {
97 const res = await req;
98 } catch(err) {
99 // AbortError!
100 }
101```
102
103Tokens, like AbortController signals, can be used to abort multiple requests. Let Flighty automate the creation and management of AbortController's for your requests. Just pass in a token and your request is then easy to abort.
104
105```js
106 const abortToken = "some token";
107 const reqOne = flighty('/pathone',{abortToken})
108 const reqTwo = flighty('/pathtwo',{abortToken})
109 const reqThree = flighty('/paththree',{abortToken})
110
111 // cancel them all!
112 flighty.abort(abortToken);
113
114```
115
116### Interceptors
117
118Drop in replacement for anybody using Frisbee interceptors or [fetch-intercept](https://www.npmjs.com/package/fetch-intercept), but with a couple of extra things:
119
120```js
121const interceptor = {
122 request: (path,options,extra,retryCount) => {
123
124 // extra is an immutable object of the data passed in when
125 // creating the request - e.g. flighty('/mypath',{myFetchOptions},{someExtraData})
126 // it doesn't get changed between interceptors if you modify it.
127
128 // retryCount is the number of times this request has been
129 // retried via res.flighty.retry() or by using the retry parameters
130 // and is also immutable between interceptors
131
132
133 return [path,options];
134 }
135}
136```
137
138Here's an example interceptor object:
139```js
140{
141request: function (path, options, extra, retryCount) {
142 // remember - extra and retryCount are immutable and will
143 // be passed to each interceptor the same
144 return [path, options];
145},
146requestError: function (err) {
147 // Handle an error occurred in the request method
148 return Promise.reject(err);
149},
150response: function (response) {
151 // do something with response (or res.flighty!)
152 return response;
153},
154responseError: function (err) {
155 // Handle error occurred in the last ran requestInterceptor, or the fetch itself
156 return Promise.reject(err);
157}
158```
159
160### Retries
161
162Flighty implements the same retry parameters found in [fetch-retry](https://www.npmjs.com/package/fetch-retry) but it adds two important features:
163
1641) Doesn't continue to retry if the request was aborted via an AbortController signal (or token)
1652) Add's an optional asynchronous `retryFn` that will be executed between retries
166
167#### Retry API
168
169* `retries` - the maximum number of retries to perform on a fetch (default 0 - do not retry)
170
171* `retryDelay` - a timeout in ms to wait between retries (default 1000ms)
172
173* `retryOn` - an array of HTTP status codes that you want to retry (default you only retry if there was a network error)
174
175* `retryFn` - a function that gets called in between the failure and the retry. This function is `await`ed so you can do some asynchronous work before the retry. Combine this with retryOn:[401] and you've got yourself a(nother) recipe to refresh JWTs (more at the end of this README):
176
177```js
178res = await api.get('/path-requiring-authentication',{
179 retries:1,
180 retryOn:[401],
181 retryFn:() => api.get('/path_to_refresh_you_token')
182})
183```
184
185The Flighty object also has a retry method to make it simply to retry a request:
186
187```js
188 let res;
189 const api = new Flighty();
190 res = await api.get('/');
191
192 if(!res.ok && res.flighty.retryCount === 0) {
193 // try it one more time...
194 res = res.flighty.retry();
195 }
196```
197
198---
199
200## API
201
202* `Flighty` - accepts an `options` object, with the following accepted options:
203
204 * `baseURI` - the default URI use to prefix all your paths
205
206 * `headers` - an object containing default headers to send with every request
207
208 * `arrayFormat` - how to stringify array in passed body. See [qs][qs-url] for available formats
209
210Upon being invoked, `Flighty` has the following methods
211
212* `jwt(token)` - Set your Bearer Authorization header via this method. Pass in a token and Flighty will add the header for you, pass in something false-y and Flighty won't automatically add an auth header (in case you want to do it yourself)
213
214* `auth(username,password)` - Set your Basic Authorization header via this method. Pass in a username and password and Flighty will add the header `Authorization Basic bas64encoded username and password` for you, pass in something false-y and Flighty won't automatically add an auth header (in case you want to do it yourself)
215
216* `abort` - method that accepts an abortToken to abort specific requests.
217
218* `abortAll` - aborts all in-progress requests controlled by this instance.
219
220* HTTP wrapper methods (e.g. get, post, put, delete, etc) require a `path` string, and accept two optional plain object arguments - `options` and `extra`
221
222 * Accepted method arguments:
223
224 * `path` **required** - the path for the HTTP request (e.g. `/v1/login`, will be prefixed with the value of `baseURI` if set)
225
226 * `options` _optional_ - everything you'd pass into fetch's [init](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch) plus optional `abortToken` and retry parameters: `retries`,`retryFn`,`retryDelay`,`retryFn`
227
228 * `extra` _optional_ object - sometimes you have some meta data about a request that you may want interceptors or other various listeners to know about. Whatever you pass in here will come out the other end attached the to `res.flighty.call` object and will also be passed along to all interceptors along the way
229
230 * List of available HTTP methods:
231
232 * `api.get(path, options, extra)` - GET
233 * `api.head(path, options, extra)` - HEAD
234 * `api.post(path, options, extra)` - POST
235 * `api.put(path, options, extra)` - PUT
236 * `api.del(path, options, extra)` - DELETE
237 * `api.options(path, options, extra)` - OPTIONS
238 * `api.patch(path, options, extra)` - PATCH
239
240* `registerInterceptor` - method that accepts an `interceptor` and calls it on every fetch. Returns a function that allows you to remove it:
241
242 ```js
243 const api = new Flighty({});
244 const undo = api.registerInterceptor(someInterceptor);
245 await api.get('/'); // your interceptor will run
246 undo(); // your interceptor is gone!
247 ```
248
249* `removeInterceptor` - method that accepts an reference to interceptor and removes it
250
251
252* `clearInterceptors` - removes all interceptors.
253
254For convenience, Flighty has exposed an `interceptor` property that has the same API as frisbee to register and unregister interceptors.
255
256---
257
258## Recipes
259
260### Throw if not 2xx recipe
261
262Don't know about you, but I found it annoying that I always had to check `res.ok` to handle my error conditions - why not just throw if the response isn't ok? Interceptor!
263
264Before:
265```js
266
267 const res = await fetch('/');
268 if(res.ok) {
269 // do some good stuff
270 } else {
271 // do some bad stuff
272 }
273
274```
275
276After:
277```js
278const api = new Flighty();
279api.registerInterceptor({
280 response:res {
281 if(!res.ok) {
282 throw res;
283 }
284 return res;
285 }
286});
287
288// Now all my responses throw if I get a non-2xx response
289try {
290 const res = await api.get('/');
291} catch(e) {
292 // response returned non-2xx
293}
294```
295
296### JWT Recipe with retry() and Interceptors
297
298```js
299const api = new Flighty();
300
301const interceptor = {
302 request:(path,options) {
303 api.jwt(path === REFRESH_ENDPOINT ? myRefreshToken : myAccessToken);
304 return [path,options]
305 },
306 response:async res {
307
308 // our only job when hitting the login path is to set the tokens locally
309 if(path === LOGIN_ENDPOINT) {
310 if(res.ok) {
311 // store your access and refresh tokens
312 setTokensLocally()
313 }
314
315 return res;
316 }
317
318 // if we get a 401 and we're not trying to refresh and this is our first retry
319 if (res.status === 401 && path !== REFRESH_TOKEN_PATH && res.flighty.retryCount === 0) {
320 try {
321 await requestToRefreshYourToken();
322 return response.flighty.retry()
323 } catch(e) {
324 // log the user out
325 }
326 }
327
328 return res;
329 }
330}
331
332```
333
334### Alternate JWT Recipe and Interceptors
335```js
336const api = new Flighty();
337
338// same request interceptor as before
339const interceptor = {
340 request:(path,options) {
341 api.jwt(path === REFRESH_ENDPOINT ? myRefreshToken : myAccessToken);
342 return [path,options]
343 }
344}
345
346const authenticatedApiRequest = (path,options,extra) => {
347 return api(
348 path,
349 {
350 ...options,
351 // retry the request 1 time
352 retries:1,
353 // if a 401 or network error is received
354 retryOn:[401],
355 // and request a new token in between
356 retryFn:() => api.get(REFRESH_TOKEN_ENDPOINT)
357 }
358 extra)
359};
360
361const myRequest = authenticatedApiRequest('/some-path-requiring-authentication');
362
363```