Created
April 24, 2017 10:16
-
-
Save maximromanyuk/2c05bb37d0e15e6b6100d2fc5fe6728e to your computer and use it in GitHub Desktop.
API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Promise from 'bluebird'; | |
import HttpError from 'standard-http-error'; | |
import { getConfiguration } from '../utils/configuration'; | |
import { getAuthenticationToken } from '../utils/authentication'; | |
const TIMEOUT = 6000; | |
/** | |
* GET a path relative to API root url. | |
* @param {String} path Relative path to the configured API endpoint | |
* @param {Boolean} suppressRedBox If true, no warning is shown on failed request | |
* @returns {Promise} of response body | |
*/ | |
export async function get(path, suppressRedBox) { | |
return bodyOf(request('get', path, null, suppressRedBox)); | |
} | |
/** | |
* POST JSON to a path relative to API root url | |
* @param {String} path Relative path to the configured API endpoint | |
* @param {Object} body Anything that you can pass to JSON.stringify | |
* @param {Boolean} suppressRedBox If true, no warning is shown on failed request | |
* @returns {Promise} of response body | |
*/ | |
export async function post(path, body, suppressRedBox) { | |
console.log('post', path, body); | |
return bodyOf(request('post', path, body, suppressRedBox)); | |
} | |
/** | |
* PUT JSON to a path relative to API root url | |
* @param {String} path Relative path to the configured API endpoint | |
* @param {Object} body Anything that you can pass to JSON.stringify | |
* @param {Boolean} suppressRedBox If true, no warning is shown on failed request | |
* @returns {Promise} of response body | |
*/ | |
export async function put(path, body, suppressRedBox) { | |
return bodyOf(request('put', path, body, suppressRedBox)); | |
} | |
/** | |
* DELETE a path relative to API root url | |
* @param {String} path Relative path to the configured API endpoint | |
* @param {Boolean} suppressRedBox If true, no warning is shown on failed request | |
* @returns {Promise} of response body | |
*/ | |
export async function del(path, suppressRedBox) { | |
return bodyOf(request('delete', path, null, suppressRedBox)); | |
} | |
/** | |
* Make arbitrary fetch request to a path relative to API root url | |
* @param {String} method One of: get|post|put|delete | |
* @param {String} path Relative path to the configured API endpoint | |
* @param {Object} body Anything that you can pass to JSON.stringify | |
* @param {Boolean} suppressRedBox If true, no warning is shown on failed request | |
*/ | |
export async function request(method, path, body, suppressRedBox) { | |
try { | |
const response = await sendRequest(method, path, body, suppressRedBox); | |
return handleResponse( | |
path, | |
response | |
); | |
} | |
catch (error) { | |
if (!suppressRedBox) { | |
logError(error, url(path), method); | |
} | |
throw error; | |
} | |
} | |
/** | |
* Takes a relative path and makes it a full URL to API server | |
*/ | |
export function url(path) { | |
const apiRoot = getConfiguration('API_ROOT'); | |
return path.indexOf('/') === 0 | |
? apiRoot + path | |
: apiRoot + '/' + path; | |
} | |
/** | |
* Constructs and fires a HTTP request | |
*/ | |
async function sendRequest(method, path, body) { | |
try { | |
const endpoint = url(path); | |
const token = await getAuthenticationToken(); | |
const headers = getRequestHeaders(body, token); | |
const options = body | |
? { method, headers, body: JSON.stringify(body) } | |
: { method, headers }; | |
return timeout(fetch(endpoint, options), TIMEOUT); | |
} catch (e) { | |
throw new Error(e); | |
} | |
} | |
/** | |
* Receives and reads a HTTP response | |
*/ | |
async function handleResponse(path, response) { | |
try { | |
const status = response.status; | |
// `fetch` promises resolve even if HTTP status indicates failure. Reroute | |
// promise flow control to interpret error responses as failures | |
if (status >= 400) { | |
const message = await getErrorMessageSafely(response); | |
throw new HttpError(status, message); | |
} | |
// parse response text | |
const responseBody = await response.text(); | |
return { | |
status: response.status, | |
headers: response.headers, | |
body: responseBody ? JSON.parse(responseBody) : null | |
}; | |
} catch (e) { | |
throw e; | |
} | |
} | |
function getRequestHeaders(body, token) { | |
const headers = body | |
? { 'Accept': 'application/json', 'Content-Type': 'application/json' } | |
: { 'Accept': 'application/json' }; | |
if (token) { | |
return { ...headers, Authorization: token }; | |
} | |
return headers; | |
} | |
// try to get the best possible error message out of a response | |
// without throwing errors while parsing | |
async function getErrorMessageSafely(response) { | |
try { | |
const body = await response.text(); | |
if (!body) { | |
return ''; | |
} | |
// Optimal case is JSON with a defined message property | |
const payload = JSON.parse(body); | |
if (payload && payload.message) { | |
return payload.message; | |
} | |
// Should that fail, return the whole response body as text | |
return body; | |
} catch (e) { | |
// Unreadable body, return whatever the server returned | |
return response._bodyInit; | |
} | |
} | |
/** | |
* Rejects a promise after `ms` number of milliseconds, it is still pending | |
*/ | |
function timeout(promise, ms) { | |
return new Promise((resolve, reject) => { | |
const timer = setTimeout(() => reject(new Error('timeout')), ms); | |
promise | |
.then(response => { | |
clearTimeout(timer); | |
resolve(response); | |
}) | |
.catch(reject); | |
}); | |
} | |
async function bodyOf(requestPromise) { | |
try { | |
const response = await requestPromise; | |
return response.body; | |
} catch (e) { | |
throw e; | |
} | |
} | |
/** | |
* Make best effort to turn a HTTP error or a runtime exception to meaningful error log message | |
*/ | |
function logError(error, endpoint, method) { | |
if (error.status) { | |
const summary = `(${error.status} ${error.statusText}): ${error._bodyInit}`; | |
console.error(`API request ${method.toUpperCase()} ${endpoint} responded with ${summary}`); | |
} | |
else { | |
console.error(`API request ${method.toUpperCase()} ${endpoint} failed with message "${error.message}"`); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment