UNPKG

14.4 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 flighty.get('/somepath').then(...)
35</script>
36```
37
38### ES5
39```js
40// no polyfill
41var flighty = require('flighty');
42
43// fetch, abort, and promise polyfills
44var flighty = require('flighty/fetch')
45
46// abort only polyfill
47var flighty = require('flighty/abort')
48```
49
50
51### ES6 (and React Native*)
52```js
53// no polyfill
54import flighty from "flighty";
55
56// fetch, abort, and promise polyfills
57import flighty from "flighty/fetch";
58
59// abort only polyfill
60import flighty from "flighty/abort";
61```
62
63**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.
64
65## Tiny
66
67Regardless 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.
68
69## Features
70
71**Drop in replacement** for fetch. This works:
72
73```js
74const res = await fetch('/somepath',{some options});
75const res = await flighty.get('/somepath',{some options});
76```
77
78This 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**.
79
80The 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.
81
82### flighty object
83
84```js
85res.flighty = {
86 method, // the method you called flighty with - e.g. get, post, put
87 retryCount, // the number of times this request has been retried
88 json, // what you'd normally get from await res.json()
89 text, // what you'd normally get from await res.text()
90 // the values the original flighty request was called with
91 call: {
92 path,
93 options,
94 extra
95 },
96 // retry method - calls the same request you made the first time again - hello JWT 401s
97 retry: async ()
98}
99
100```
101
102### Abort
103
104Flighty 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!).
105
106Aborting 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:
107
108```js
109 const controller = new AbortController();
110 const req = fetch('/',{signal:controller.signal})
111
112 controller.abort();
113
114 try {
115 const res = await req;
116 } catch(err) {
117 // AbortError!
118 }
119
120```
121
122Flighty allows you to pass in a token (any Symbol) and then call `abort(token)` to cancel the request.
123
124```js
125 const req = flighty.get('/',{abortToken:"my token"});
126 flighty.abort("my token");
127
128 try {
129 const res = await req;
130 } catch(err) {
131 // AbortError!
132 }
133```
134
135Tokens, 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.
136
137```js
138 const abortToken = "some token";
139 const reqOne = flighty('/pathone',{abortToken})
140 const reqTwo = flighty('/pathtwo',{abortToken})
141 const reqThree = flighty('/paththree',{abortToken})
142
143 // cancel them all!
144 flighty.abort(abortToken);
145
146```
147
148### Interceptors
149
150Drop 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:
151
152```js
153const interceptor = {
154 request: (path,options,extra,retryCount) => {
155
156 // extra is an immutable object of the data passed in when
157 // creating the request - e.g. flighty('/mypath',{myFetchOptions},{someExtraData})
158 // it doesn't get changed between interceptors if you modify it.
159
160 // retryCount is the number of times this request has been
161 // retried via res.flighty.retry() or by using the retry parameters
162 // and is also immutable between interceptors
163
164
165 return [path,options];
166 }
167}
168```
169
170Here's an example interceptor object:
171```js
172{
173 request: function (path, options, extra, retryCount) {
174 // remember - extra and retryCount are immutable and will
175 // be passed to each interceptor the same
176 return [path, options];
177 },
178 requestError: function (err) {
179 // Handle an error occurred in the request method
180 return Promise.reject(err);
181 },
182 response: function (response) {
183 // do something with response (or res.flighty!)
184 return response;
185 },
186 responseError: function (err) {
187 // Handle error occurred in the last ran requestInterceptor, or the fetch itself
188 return Promise.reject(err);
189 }
190}
191```
192
193### Retries
194
195Flighty implements the same retry parameters found in [fetch-retry](https://www.npmjs.com/package/fetch-retry) but it adds two important features:
196
1971) Doesn't continue to retry if the request was aborted via an AbortController signal (or token)
1982) Adds an optional asynchronous `retryFn` that will be executed between retries
199
200#### Retry API
201
202* `retries` - the maximum number of retries to perform on a fetch (default 0 - do not retry)
203
204* `*retryDelay` - a timeout in ms to wait between retries (default 1000ms)
205
206* `retryOn` - an array of HTTP status codes that you want to retry (default you only retry if there was a network error)
207
208* `*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):
209
210***Note:** The `retryDelay` parameter will be ignored if `retryFn` is declared. If you're using `retryFn` it's up to you to handle the delay, if any, between retries.
211
212```js
213res = await flighty.get('/path-requiring-authentication',{
214 retries:1,
215 retryOn:[401],
216 retryFn:() => flighty.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 res = await flighty.get('/');
225
226 if(!res.ok && res.flighty.retryCount === 0) {
227 // try it one more time...
228 res = res.flighty.retry();
229 }
230```
231
232---
233
234## API
235
236* `flighty` - default export - an instance of `Flighty`. It has a `create` method that can be used to instantiate other instances of `Flighty`. The `create` method accepts an object with can contain the following options:
237
238 * `baseURI` - the default URI use to prefix all your paths
239
240 * `headers` - an object containing default headers to send with every request
241
242 * `arrayFormat` - how to stringify array in passed body. See [qs](https://www.npmjs.com/package/qs) for available formats
243
244Instances of `Flighty` contain the following methods:
245
246* `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)
247
248* `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)
249
250* `abort` - method that accepts an abortToken to abort specific requests.
251
252* `abortAll` - aborts all in-progress requests controlled by this instance.
253
254* HTTP wrapper methods (e.g. get, post, put, delete, etc) require a `path` string, and accept two optional plain object arguments - `options` and `extra`
255
256 * Accepted method arguments:
257
258 * `path` **required** - the path for the HTTP request (e.g. `/v1/login`, will be prefixed with the value of `baseURI` if set)
259
260 * `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`
261
262 * `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
263
264 * List of available HTTP methods:
265
266 * `api.get(path, options, extra)` - GET
267 * `api.head(path, options, extra)` - HEAD
268 * `api.post(path, options, extra)` - POST
269 * `api.put(path, options, extra)` - PUT
270 * `api.del(path, options, extra)` - DELETE
271 * `api.options(path, options, extra)` - OPTIONS
272 * `api.patch(path, options, extra)` - PATCH
273
274* `registerInterceptor` - method that accepts an `interceptor` and calls it on every fetch. Returns a function that allows you to remove it:
275
276 ```js
277 const api = new Flighty({});
278 const undo = api.registerInterceptor(someInterceptor);
279 await api.get('/'); // your interceptor will run
280 undo(); // your interceptor is gone!
281 ```
282
283* `removeInterceptor` - method that accepts an reference to interceptor and removes it
284
285
286* `clearInterceptors` - removes all interceptors.
287
288For convenience, Flighty has exposed an `interceptor` property that has the same API as frisbee to register and unregister interceptors.
289
290## Unit testing
291
292Keep 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).
293
294---
295
296## Recipes
297
298### Throw if not 2xx recipe
299
300Don'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!
301
302Before:
303```js
304
305 const res = await fetch('/');
306 if(res.ok) {
307 // do some good stuff
308 } else {
309 // do some bad stuff
310 }
311
312```
313
314After:
315```js
316flighty.registerInterceptor({
317 response:res => {
318 if(!res.ok) {
319 throw res;
320 }
321 return res;
322 }
323});
324
325// Now all my responses throw if I get a non-2xx response
326try {
327 const res = await flighty.get('/');
328} catch(e) {
329 // response returned non-2xx
330}
331```
332
333### JWT Recipe with retry() and Interceptors
334
335```js
336
337const interceptor = {
338 request: (path,options) => {
339 flighty.jwt(path === REFRESH_ENDPOINT ? myRefreshToken : myAccessToken);
340 return [path,options]
341 },
342 response: async res => {
343
344 // our only job when hitting the login path is to set the tokens locally
345 if(path === LOGIN_ENDPOINT) {
346 if(res.ok) {
347 // store your access and refresh tokens
348 setTokensLocally()
349 }
350
351 return res;
352 }
353
354 // if we get a 401 and we're not trying to refresh and this is our first retry
355 if (res.status === 401 && path !== REFRESH_TOKEN_PATH && res.flighty.retryCount === 0) {
356 try {
357 await requestToRefreshYourToken();
358 return response.flighty.retry()
359 } catch(e) {
360 // log the user out
361 }
362 }
363
364 return res;
365 }
366}
367
368```
369
370### Alternate JWT Recipe and Interceptors
371```js
372
373// same request interceptor as before
374const interceptor = {
375 request:(path,options) => {
376 flighty.jwt(path === REFRESH_ENDPOINT ? myRefreshToken : myAccessToken);
377 return [path,options]
378 }
379}
380
381const authenticatedApiRequest = (path,options,extra) => {
382 return flighty(
383 path,
384 {
385 ...options,
386 // retry the request 1 time
387 retries:1,
388 // if a 401 or network error is received
389 retryOn:[401],
390 // and request a new token in between
391 retryFn:() => flighty.get(REFRESH_TOKEN_ENDPOINT)
392 }
393 extra)
394};
395
396const myRequest = authenticatedApiRequest('/some-path-requiring-authentication');
397```
398
399## Contributing
400
401PRs and ideas welcome!
\No newline at end of file