UNPKG

14.1 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[![install size](https://packagephobia.now.sh/badge?p=flighty&flightyCB=1)](https://packagephobia.now.sh/result?p=flighty)
7[![codecov](https://codecov.io/gh/akmjenkins/flighty/branch/master/graph/badge.svg)](https://codecov.io/gh/akmjenkins/flighty)
8[![Build Status](https://travis-ci.org/akmjenkins/flighty.svg?branch=master)](https://travis-ci.org/akmjenkins/flighty)
9
10Simple (and tiny) fetch wrapper with nifty features such as intercepts, ***easy*** aborts, and retries, for everywhere - that's browser, react-native, and ES5/6 front-ends.
11
12## Motivation
13
14Yet another fetch wrapping library? Well, various fetch-wrapping libraries have some of the above features, but none have them all.
15
16More importantly, almost all fetch wrapping libraries investigated include their polyfills right in the main packages (or don't include polyfills 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.
17
18Everything you'll need is included in Flighty, it's just a matter of figuring out what you need. Running in a fetch-unknown environment - use flighty/fetch. You know you'll already have a fetch but unsure of AbortController? Use flighty/abort. Supporting the latest and greatest? Just import plain ol' flighty.
19
20### Browser
21```html
22<!-- no polyfills -->
23<script src="https://unpkg.com/flighty"></script>
24
25<!-- fetch, abort, and promise polyfills -->
26<script src="https://unpkg.com/flighty/fetch"></script>
27
28<!-- abort only polyfill -->
29<script src="https://unpkg.com/flighty/abort"></script>
30
31
32<script>
33 // no matter which package you choose
34 var api = new Flighty({baseURI:'https://myapi.com'})
35 api.get('/somepath').then(...)
36</script>
37```
38
39### ES5
40```js
41// no polyfill
42var Flighty = require('flighty');
43
44// fetch, abort, and promise polyfills
45var Flighty = require('flighty/fetch')
46
47// abort only polyfill
48var Flighty = require('flighty/abort');
49```
50
51
52### ES6 (and React Native*)
53```js
54// no polyfill
55import Flighty from "Flighty";
56
57// fetch, abort, and promise polyfills
58import Flighty from "flighty/fetch";
59
60// abort only polyfill
61import Flighty from "flighty/abort";
62```
63
64**Note:** React Natives import from Flighty includes the AbortController polyfill. If React Native ever updates it's fetch, Flighty will remove this. If you do `import Flighty from "flighty/abort"` in React Native you'll get the same package as `import Flighty from "flighty"`, so it's recommended to do the latter.
65
66## Tiny
67
68Regardless of the package and implementation you choose, flighty is **tiny**. The biggest implementation (which is the browser build that has all polyfills) is *less than 9kb* minified and gzipped.
69
70## Features
71
72**Drop in replacement** for fetch. This works:
73
74```js
75const res = await fetch('/somepath',{some options});
76
77const api = new Flighty({some options});
78const res = await api.get('/somepath',{some options});
79```
80
81This 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**.
82
83The 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 then refactoring Flighty into/out of your codebase becomes a breeze.
84
85### flighty object
86
87```js
88res.flighty = {
89 method, // the method you called flighty with - e.g. get, post, put
90 retryCount, // the number of times this request has been retried
91 json, // what you'd normally get from await res.json()
92 text, // what you'd normally get from await res.text()
93 // the values the original flighty request was called with
94 call: {
95 path,
96 options,
97 extra
98 },
99 // retry method - calls the same request you made the first time again - hello JWT 401s
100 retry: async ()
101}
102
103```
104
105### Abort
106
107Flighty 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!).
108
109Aborting 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:
110
111```js
112 const controller = new AbortController();
113 const req = fetch('/',{signal:controller.signal})
114
115 controller.abort();
116
117 try {
118 const res = await req;
119 } catch(err) {
120 // AbortError!
121 }
122
123```
124
125Flighty allows you to pass in a token (any Symbol) and then call `abort(token)` to cancel the request.
126
127```js
128 const req = flighty.get('/',{abortToken:"my token"});
129 api.abort("my token");
130
131 try {
132 const res = await req;
133 } catch(err) {
134 // AbortError!
135 }
136```
137
138Tokens, 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.
139
140```js
141 const abortToken = "some token";
142 const reqOne = flighty('/pathone',{abortToken})
143 const reqTwo = flighty('/pathtwo',{abortToken})
144 const reqThree = flighty('/paththree',{abortToken})
145
146 // cancel them all!
147 flighty.abort(abortToken);
148
149```
150
151### Interceptors
152
153Drop in replacement for anybody using [Frisbee](https://www.npmjs.com/package/frisbee) interceptors or [fetch-intercept](https://www.npmjs.com/package/fetch-intercept), but with a couple of extra things:
154
155```js
156const interceptor = {
157 request: (path,options,extra,retryCount) => {
158
159 // extra is an immutable object of the data passed in when
160 // creating the request - e.g. flighty('/mypath',{myFetchOptions},{someExtraData})
161 // it doesn't get changed between interceptors if you modify it.
162
163 // retryCount is the number of times this request has been
164 // retried via res.flighty.retry() or by using the retry parameters
165 // and is also immutable between interceptors
166
167
168 return [path,options];
169 }
170}
171```
172
173Here's an example interceptor object:
174```js
175{
176request: function (path, options, extra, retryCount) {
177 // remember - extra and retryCount are immutable and will
178 // be passed to each interceptor the same
179 return [path, options];
180},
181requestError: function (err) {
182 // Handle an error occurred in the request method
183 return Promise.reject(err);
184},
185response: function (response) {
186 // do something with response (or res.flighty!)
187 return response;
188},
189responseError: function (err) {
190 // Handle error occurred in the last ran requestInterceptor, or the fetch itself
191 return Promise.reject(err);
192}
193```
194
195### Retries
196
197Flighty implements the same retry parameters found in [fetch-retry](https://www.npmjs.com/package/fetch-retry) but it adds two important features:
198
1991) Doesn't continue to retry if the request was aborted via an AbortController signal (or token)
2002) Adds an optional asynchronous `retryFn` that will be executed between retries
201
202#### Retry API
203
204* `retries` - the maximum number of retries to perform on a fetch (default 0 - do not retry)
205
206* `retryDelay` - a timeout in ms to wait between retries (default 1000ms)
207
208* `retryOn` - an array of HTTP status codes that you want to retry (default you only retry if there was a network error)
209
210* `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):
211
212```js
213res = await api.get('/path-requiring-authentication',{
214 retries:1,
215 retryOn:[401],
216 retryFn:() => api.get('/path_to_refresh_you_token')
217})
218```
219
220The Flighty object also has a retry method to make it simply to retry a request:
221
222```js
223 let res;
224 const api = new Flighty();
225 res = await api.get('/');
226
227 if(!res.ok && res.flighty.retryCount === 0) {
228 // try it one more time...
229 res = res.flighty.retry();
230 }
231```
232
233---
234
235## API
236
237* `Flighty` - accepts an `options` object, with the following accepted options:
238
239 * `baseURI` - the default URI use to prefix all your paths
240
241 * `headers` - an object containing default headers to send with every request
242
243 * `arrayFormat` - how to stringify array in passed body. See [qs](https://www.npmjs.com/package/qs) for available formats
244
245Upon being invoked, `Flighty` has the following methods
246
247* `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)
248
249* `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)
250
251* `abort` - method that accepts an abortToken to abort specific requests.
252
253* `abortAll` - aborts all in-progress requests controlled by this instance.
254
255* HTTP wrapper methods (e.g. get, post, put, delete, etc) require a `path` string, and accept two optional plain object arguments - `options` and `extra`
256
257 * Accepted method arguments:
258
259 * `path` **required** - the path for the HTTP request (e.g. `/v1/login`, will be prefixed with the value of `baseURI` if set)
260
261 * `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`
262
263 * `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
264
265 * List of available HTTP methods:
266
267 * `api.get(path, options, extra)` - GET
268 * `api.head(path, options, extra)` - HEAD
269 * `api.post(path, options, extra)` - POST
270 * `api.put(path, options, extra)` - PUT
271 * `api.del(path, options, extra)` - DELETE
272 * `api.options(path, options, extra)` - OPTIONS
273 * `api.patch(path, options, extra)` - PATCH
274
275* `registerInterceptor` - method that accepts an `interceptor` and calls it on every fetch. Returns a function that allows you to remove it:
276
277 ```js
278 const api = new Flighty({});
279 const undo = api.registerInterceptor(someInterceptor);
280 await api.get('/'); // your interceptor will run
281 undo(); // your interceptor is gone!
282 ```
283
284* `removeInterceptor` - method that accepts an reference to interceptor and removes it
285
286
287* `clearInterceptors` - removes all interceptors.
288
289For convenience, Flighty has exposed an `interceptor` property that has the same API as frisbee to register and unregister interceptors.
290
291## Unit testing
292
293Keep 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).
294
295---
296
297## Recipes
298
299### Throw if not 2xx recipe
300
301Don'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!
302
303Before:
304```js
305
306 const res = await fetch('/');
307 if(res.ok) {
308 // do some good stuff
309 } else {
310 // do some bad stuff
311 }
312
313```
314
315After:
316```js
317const api = new Flighty();
318api.registerInterceptor({
319 response:res => {
320 if(!res.ok) {
321 throw res;
322 }
323 return res;
324 }
325});
326
327// Now all my responses throw if I get a non-2xx response
328try {
329 const res = await api.get('/');
330} catch(e) {
331 // response returned non-2xx
332}
333```
334
335### JWT Recipe with retry() and Interceptors
336
337```js
338const api = new Flighty();
339
340const interceptor = {
341 request: (path,options) => {
342 api.jwt(path === REFRESH_ENDPOINT ? myRefreshToken : myAccessToken);
343 return [path,options]
344 },
345 response: async res => {
346
347 // our only job when hitting the login path is to set the tokens locally
348 if(path === LOGIN_ENDPOINT) {
349 if(res.ok) {
350 // store your access and refresh tokens
351 setTokensLocally()
352 }
353
354 return res;
355 }
356
357 // if we get a 401 and we're not trying to refresh and this is our first retry
358 if (res.status === 401 && path !== REFRESH_TOKEN_PATH && res.flighty.retryCount === 0) {
359 try {
360 await requestToRefreshYourToken();
361 return response.flighty.retry()
362 } catch(e) {
363 // log the user out
364 }
365 }
366
367 return res;
368 }
369}
370
371```
372
373### Alternate JWT Recipe and Interceptors
374```js
375const api = new Flighty();
376
377// same request interceptor as before
378const interceptor = {
379 request:(path,options) => {
380 api.jwt(path === REFRESH_ENDPOINT ? myRefreshToken : myAccessToken);
381 return [path,options]
382 }
383}
384
385const authenticatedApiRequest = (path,options,extra) => {
386 return api(
387 path,
388 {
389 ...options,
390 // retry the request 1 time
391 retries:1,
392 // if a 401 or network error is received
393 retryOn:[401],
394 // and request a new token in between
395 retryFn:() => api.get(REFRESH_TOKEN_ENDPOINT)
396 }
397 extra)
398};
399
400const myRequest = authenticatedApiRequest('/some-path-requiring-authentication');
401```