├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── src ├── fetch-wrapper.js ├── index.js └── request-creator-helpers.js └── test ├── fetch-wrapper.test.js └── runner.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | node_modules/ 14 | lib/ 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nikhila Ravi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fetch-wrapper 2 | Wrapper around isomorphic-fetch for resending fetch request if there is an error. 3 | 4 | [![NPM](https://nodei.co/npm-dl/fetch-wrapper.png?months=3)](https://nodei.co/npm/fetch-wrapper/) 5 | 6 | ## Getting Started 7 | 8 | - [Installation](#installation) 9 | - [Request Wrapper](#request-wrapper) 10 | - [Request Creator Helpers](#request-creator-helpers) 11 | - [Example Usage](#example-usage) 12 | - [Credits](#credits) 13 | 14 | ### Installation 15 | 16 | ```bash 17 | $ npm i fetch-wrapper --save 18 | 19 | ``` 20 | 21 | To run the tests first clone the repo: 22 | 23 | ```bash 24 | $ git clone https://github.com/nikhilaravi/fetch-wrapper.git 25 | 26 | ``` 27 | 28 | Run the tests: 29 | 30 | ```bash 31 | $ npm test 32 | 33 | ``` 34 | 35 | ## Request wrapper 36 | 37 | The `sendRequest` function retries the fetch request if there is an error. 38 | 39 | | Param | Default | Type | Description | 40 | | :------------ |:---------------:| :---------------:| :-----| 41 | | options | `{}` |`object` | REQUIRED: Object specifying the request, onSuccess and onError functions. See below | 42 | | retryIntervals | `[1000]` |`array of numbers` | OPTIONAL: Time intervals at which to retry the fetch request | 43 | | attempt | `0` |`number` | Do not need to specify | 44 | 45 | ### options object 46 | 47 | | Key | Type | Description | 48 | | :------------ |:---------------:| :---------------:| :-----| 49 | | request |`function` | function returns a fetch request. Can be created using the create-request helpers (see create-request.js) | 50 | | responseType | enum (`text`,`json`) | Format of the response. Used to parse the response body using either 'response.text' or 'response.json' methods | 51 | | onSuccess(response) |`function` | function to be called with the response when the fetch request returns successfully | 52 | | onError(response) |`function` | function to be called when there is an error in the fetch request | 53 | 54 | `options.onError` will only be called if there is an: 55 | - error in the fetch request 56 | - error in options.onSuccess function (e.g. redux error) 57 | 58 | The error object passed to onError is of the form 59 | ```js 60 | { 61 | status: '', //either a status code or 'error' 62 | message: '' 63 | } 64 | ``` 65 | 66 | `options.onSuccess` will be called on 67 | - successful requests 68 | - network errors e.g. 404/500 69 | 70 | The response passed to onSuccess is either the response data (json/text) or in the case of a network/server/parsing error, an error object of the form 71 | ```js 72 | { 73 | status: '', //'error' 74 | message: '' // e.g. 'Invalid Response Type' or 'No response body' 75 | } 76 | ``` 77 | 78 | ## Request Creator Helpers 79 | 80 | Helper functions that return a function that send a fetch request 81 | The promise returned by fetch is then resolved inside the `sendRequest` function. 82 | 83 | Options available for sending get, post and put requests with and without authentication. 84 | 85 | The parameters for each helper are outline below in the order they need to be specified. 86 | 87 | ### getReq 88 | | Param | Default | Type | Description | 89 | | :------------ |:---------------:| :---------------:| :-----| 90 | | url | `undefined` |`string` | url of the request | 91 | | header | `{}` |`object` | Optional header options | 92 | 93 | 94 | ### postReq 95 | | Param | Default | Type | Description | 96 | | :------------ |:---------------:| :---------------:| :-----| 97 | | url | `undefined` |`string` | url of the request | 98 | | data | `null` |`object` | request body which will be stringified | 99 | | header | `{}` |`object` | Optional header options | 100 | 101 | ### putReq 102 | | Param | Default | Type | Description | 103 | | :------------ |:---------------:| :---------------:| :-----| 104 | | url | `undefined` |`string` | url of the request | 105 | | data | `null` |`object` | request body which will be stringified | 106 | | header | `{}` |`object` | Optional header options | 107 | 108 | ### getAuthReq 109 | | Param | Default | Type | Description | 110 | | :------------ |:---------------:| :---------------:| :-----| 111 | | url | `undefined` |`string` | url of the request | 112 | | token | `undefined` |`string` | Authentication token which will be set to the 'Authorization' key in the request header object | 113 | | header | `{}` |`object` | Optional further header options | 114 | 115 | ### postAuthReq 116 | | Param | Default | Type | Description | 117 | | :------------ |:---------------:| :---------------:| :-----| 118 | | url | `undefined` |`string` | url of the request | 119 | | data | `null` |`object` | request body which will be stringified | 120 | | token | `undefined` |`string` | Authentication token which will be set to the 'Authorization' key in the request header object | 121 | | header | `{}` |`object` | Optional further header options | 122 | 123 | ### putAuthReq 124 | | Param | Default | Type | Description | 125 | | :------------ |:---------------:| :---------------:| :-----| 126 | | url | `undefined` |`string` | url of the request | 127 | | data | `null` |`object` | request body which will be stringified | 128 | | token | `undefined` |`string` | Authentication token which will be set to the 'Authorization' key in the request header object | 129 | | header | `{}` |`object` | Optional further header options | 130 | 131 | ## Example usage 132 | 133 | ```js 134 | import { postReq, sendRequest } from 'fetch-wrapper' 135 | 136 | sendRequest({ 137 | request: postReq('http://localhost:9009/login', {name: 'name'}), //this should be a function that returns a fetch request 138 | responseType: 'json' 139 | onSuccess: json => { //on success code here }, 140 | onError: error => { //on error code here } 141 | }) 142 | 143 | ``` 144 | 145 | ## Credits 146 | Collaborators: @besartshyti 147 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetch-wrapper", 3 | "version": "1.0.2", 4 | "description": "Wrapper around isomorphic-fetch to retry the request if there is an error", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "tape test/runner.js", 8 | "compile": "babel -d lib/ src/", 9 | "prepublish": "npm run compile" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/nikhilaravi/fetch-wrapper.git" 14 | }, 15 | "keywords": [ 16 | "fetch", 17 | "react-native", 18 | "request" 19 | ], 20 | "devDependencies": { 21 | "babel-core": "^6.3.17", 22 | "babel-plugin-transform-object-rest-spread": "^6.1.18", 23 | "babel-preset-es2015": "^6.1.18", 24 | "babel-cli": "^6.4.5", 25 | "nock": "^5.2.1", 26 | "tape": "^4.2.2" 27 | }, 28 | "author": "nikhilaravi", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/nikhilaravi/fetch-wrapper/issues" 32 | }, 33 | "homepage": "https://github.com/nikhilaravi/fetch-wrapper#readme", 34 | "dependencies": { 35 | "isomorphic-fetch": "^2.2.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/fetch-wrapper.js: -------------------------------------------------------------------------------- 1 | export default function fetchWrapper(options={}, attempt=0, retryIntervals=retryIntervals) { 2 | const onFetchSuccess = options.onSuccess; 3 | const onFetchFail = onFail.bind(null, options, attempt, options.retryIntervals || retryIntervals); 4 | const onError = handleError.bind(null, options, attempt) 5 | 6 | options.request() 7 | .then(parseResponse.bind(null, options), onFetchFail) // catch errors from fetch request 8 | .then(onFetchSuccess, onError) // catch errors from status codes or parsing data in onFetchComplete 9 | .catch(onError) // catch errors from onSuccess or parsing data 10 | } 11 | 12 | const retryIntervals = [1000]; // default value of the time intervals at which to retry the request 13 | 14 | export const onFail = (options, attempt, error) => { 15 | if (retryIntervals[attempt]) { 16 | setTimeout( 17 | () => fetchWrapper(options, ++attempt), 18 | retryIntervals[attempt] 19 | ) 20 | } else { 21 | const status = error.response ? error.response.status : 'error'; // Treat network errors without responses as 500s (internal server error). 22 | const message = error.message; 23 | return options.onError({status, message}); 24 | } 25 | } 26 | 27 | export const parseResponse = (options, res) => { 28 | const { responseType: type } = options; 29 | if (type && type == 'text') { 30 | return res.text(); 31 | } else if (type && type == 'json') { 32 | return res.json(); 33 | } else { 34 | throw new Error('Invalid Response Type'); 35 | } 36 | } 37 | 38 | const handleError = (options, attempt, error) => { 39 | const errorMessage = error.toString(); 40 | if (errorMessage === 'SyntaxError: Unexpected end of input') { 41 | options.onSuccess({status: 'error', message: 'No response body'}) // error in res.json/res.text 42 | } else if (errorMessage === 'Error: Invalid Response Type') { 43 | options.onSuccess({status: 'error', message: 'Invalid Response Type'}) // error in options for fetch wrapper 44 | } else { 45 | return options.onError({status: 'error', message: errorMessage}) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as requestHelpers from './request-creator-helpers.js'; 2 | import fetchWrapper from './fetch-wrapper.js'; 3 | 4 | export const postReq = requestHelpers.postReq; 5 | export const getReq = requestHelpers.getReq; 6 | export const putReq = requestHelpers.putReq; 7 | export const postAuthReq = requestHelpers.postAuthReq; 8 | export const putAuthReq = requestHelpers.putAuthReq; 9 | export const getAuthReq = requestHelpers.getAuthReq; 10 | export const sendRequest = fetchWrapper; 11 | 12 | /** 13 | EXAMPLE USAGE 14 | 15 | sendRequest({ 16 | request: postReq('localhost:9009/login', {name: 'name'}), //this should be a function that returns a fetch request 17 | responseType: 'json' 18 | onSuccess: json => {}, 19 | onError: (error) => {} 20 | }) 21 | **/ 22 | -------------------------------------------------------------------------------- /src/request-creator-helpers.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | /** 4 | * Helper Functions that return a function that send a fetch request 5 | * The promise returned by fetch is then resolved in the fetch wrapper (fetch-wrapper.js) 6 | **/ 7 | 8 | /** 9 | * Create an authenticated POST request 10 | * 11 | * @param {string} - url 12 | * @param {object} - data (request body) 13 | * @param {string} - token (for authentication) 14 | * @param {object} - header - optionally pass in further header options 15 | * 16 | **/ 17 | 18 | export const postAuthReq = (url, data, token, header) => { 19 | return () => fetch(url, post(reqOptions(data, {...header, ...authHeader(token)}))) 20 | } 21 | 22 | /** 23 | * Create an authenticated PUT request 24 | * 25 | * @param {string} - url 26 | * @param {object} - data (request body) 27 | * @param {string} - token (for authentication) 28 | * @param {object} - header - optionally pass in further header options 29 | * 30 | **/ 31 | 32 | export const putAuthReq = (url, data, token, header) => { 33 | return () => fetch(url, put(reqOptions(data, {...header, ...authHeader(token)}))) 34 | } 35 | 36 | /** 37 | * Create an authenticated GET request 38 | * 39 | * @param {string} - url 40 | * @param {string} - token (for authentication) 41 | * @param {object} - header - optionally pass in further header options 42 | * 43 | **/ 44 | 45 | export const getAuthReq = (url, token, header) => { 46 | return () => fetch(url, get(reqOptions(null, {...header, ...authHeader(token)}))) 47 | } 48 | 49 | /** 50 | * Create an unauthenticated POST request 51 | * 52 | * @param {string} - url 53 | * @param {object} - data (request body) 54 | * @param {object} - header - optionally pass in further header options 55 | * 56 | **/ 57 | 58 | export const postReq = (url, data, header) => { 59 | return () => fetch(url, post(reqOptions(data), header)) 60 | } 61 | 62 | /** 63 | * Create an unauthenticated PUT request 64 | * 65 | * @param {string} - url 66 | * @param {object} - data (request body) 67 | * @param {object} - header - optionally pass in further header options 68 | * 69 | **/ 70 | 71 | export const putReq = (url, data, header) => { 72 | return () => fetch(url, put(reqOptions(data), header)) 73 | } 74 | 75 | /** 76 | * Create an unauthenticated GET request 77 | * 78 | * @param {string} - url 79 | * @param {object} - header - optionally pass in further header options 80 | * 81 | **/ 82 | 83 | export const getReq = (url, header) => { 84 | return () => fetch(url, get(reqOptions(null, header))) 85 | } 86 | 87 | // Helper functions to format the request options 88 | 89 | const reqOptions = (data=null, headers={}) => { 90 | const reqHeader = { 91 | headers: { 92 | 'Content-Type': 'application/json', 93 | ...headers, 94 | } 95 | }; 96 | return data ? { ...reqHeader, body: JSON.stringify(data) } : reqHeader 97 | }; 98 | 99 | const post = reqOptions => ({ 100 | ...reqOptions, 101 | method: 'POST' 102 | }) 103 | 104 | const put = reqOptions => ({ 105 | ...reqOptions, 106 | method: 'PUT' 107 | }) 108 | 109 | const get = reqOptions => ({ 110 | ...reqOptions, 111 | method: 'GET' 112 | }) 113 | 114 | const authHeader = token => { 115 | if (!token) { 116 | throw new Error('Token required to send an authenticated request'); 117 | } else { 118 | return { 'Authorization': token } 119 | } 120 | } 121 | 122 | export const helpers = { 123 | reqOptions, 124 | post, 125 | put, 126 | get, 127 | authHeader, 128 | } 129 | -------------------------------------------------------------------------------- /test/fetch-wrapper.test.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import nock from 'nock'; 3 | import { getReq, sendRequest } from '../index.js'; 4 | 5 | //mock error object sent from own server 6 | const err = { 7 | status: 'error', 8 | message: 'Sorry there was a problem' 9 | }; 10 | 11 | /** 12 | options.onError will only be called if there is an: 13 | - error in the fetch request 14 | - error in options.onSuccess function (e.g. redux error) 15 | **/ 16 | 17 | test('fetch-wrapper:options.onError => fetch request error calls onError with 500 status code ', t => { 18 | 19 | sendRequest({ 20 | request: getReq('http://ghghjhjkhjhskjhdfkhs.com'), 21 | responseType: 'json', 22 | onSuccess: () => {}, 23 | onError: (error) => { 24 | //status codes sent in error objects to onError function 25 | t.equal(error.status, 'error'); 26 | t.end() 27 | }, 28 | }); 29 | 30 | }); 31 | 32 | test('fetch-wrapper:options.onError => error in response', t => { 33 | 34 | nock('http://localhost:8000') 35 | .get('/test') 36 | .times(2) 37 | .replyWithError(err) 38 | 39 | sendRequest({ 40 | request: getReq('http://localhost:8000/test'), 41 | responseType: 'json', 42 | onSuccess: () => {}, 43 | onError: (error) => { 44 | t.equal(error.status, 'error'); 45 | t.end(); 46 | } 47 | }); 48 | 49 | }); 50 | 51 | 52 | test('fetch-wrapper:options.onError => Error in options.onSuccess handler', t => { 53 | 54 | nock('http://localhost:8000') 55 | .get('/') 56 | .reply(200, {name: 'name'}) 57 | 58 | sendRequest({ 59 | request: getReq('http://localhost:8000/'), 60 | responseType: 'json', 61 | onSuccess: (res) => { 62 | throw new Error("Error in onSuccess function") 63 | }, 64 | onError: error => { 65 | t.equal(error.message, 'Error: Error in onSuccess function'); 66 | t.end(); 67 | } 68 | }); 69 | 70 | }); 71 | 72 | 73 | /** 74 | options.onSuccess will be called on 75 | - successful requests 76 | - network errors e.g. 404/ 77 | **/ 78 | 79 | test('fetch-wrapper:options.onSuccess => 404 error calls onSuccess after 1 attempt', t => { 80 | 81 | nock('http://localhost:8000') 82 | .get('/') 83 | .times(1) 84 | .reply(404, err) 85 | 86 | sendRequest({ 87 | request: getReq('http://localhost:8000/'), 88 | responseType: 'json', 89 | onSuccess: (json) => { 90 | t.deepEqual(json, err); 91 | t.end() 92 | }, 93 | onError: () => {} 94 | }); 95 | 96 | }); 97 | 98 | test('fetch-wrapper:options.onSuccess => 200 success after 1 attempt', t => { 99 | 100 | nock('http://localhost:8000') 101 | .get('/login') 102 | .times(1) 103 | .reply(200, {name: 'name'}) 104 | 105 | sendRequest({ 106 | request: getReq('http://localhost:8000/login'), 107 | responseType: 'json', 108 | onSuccess: res => { 109 | t.deepEqual(res, {name: 'name'}); 110 | t.end(); 111 | }, 112 | onError: () => {} 113 | }); 114 | 115 | }); 116 | 117 | test('fetch-wrapper:options.onSuccess => 500 server error, no error object in response', t => { 118 | 119 | nock('http://localhost:8000') 120 | .get('/login') 121 | .times(4) 122 | .reply(500) 123 | 124 | sendRequest({ 125 | request: getReq('http://localhost:8000/login'), 126 | responseType: 'json', 127 | onSuccess: (json) => { 128 | t.deepEqual(json, {status: 'error', message: 'No response body'}); 129 | t.end() 130 | }, 131 | onError: () => {} 132 | }); 133 | 134 | }); 135 | 136 | test('fetch-wrapper:options.onSuccess => 500 server error, error object in response', t => { 137 | 138 | nock('http://localhost:8000') 139 | .get('/server') 140 | .times(1) 141 | .reply(500, err) 142 | 143 | sendRequest({ 144 | request: getReq('http://localhost:8000/server'), 145 | responseType: 'json', 146 | onSuccess: (json) => { 147 | t.deepEqual(json, err); 148 | t.end() 149 | }, 150 | onError: () => {} 151 | }); 152 | 153 | }); 154 | 155 | test('fetch-wrapper:options.onSuccess => Parse data Error: no response type', t => { 156 | 157 | nock('http://localhost:8000') 158 | .get('/') 159 | .times(1) 160 | .reply(200, {name: 'name'}) 161 | 162 | sendRequest({ 163 | request: getReq('http://localhost:8000/'), 164 | onSuccess: json => { 165 | t.deepEqual(json, {status: 'error', message: 'Invalid Response Type'}); 166 | t.end(); 167 | }, 168 | onError: () => {} 169 | }); 170 | 171 | }); 172 | 173 | test('fetch-wrapper:options.onSuccess => Parse data Error: invalid response type', t => { 174 | 175 | nock('http://localhost:8000') 176 | .get('/') 177 | .times(1) 178 | .reply(200, {name: 'name'}) 179 | 180 | sendRequest({ 181 | request: getReq('http://localhost:8000/'), 182 | responseType: 'form', 183 | onSuccess: () => {}, 184 | onSuccess: json => { 185 | t.deepEqual(json, {status: 'error', message: 'Invalid Response Type'}); 186 | t.end(); 187 | }, 188 | onError: () => {} 189 | }); 190 | 191 | }); 192 | -------------------------------------------------------------------------------- /test/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-core/register')({ 4 | presets: ['es2015'], 5 | plugins: ['transform-object-rest-spread'] 6 | }); 7 | 8 | require('./fetch-wrapper.test.js'); 9 | --------------------------------------------------------------------------------