Created
April 22, 2020 12:31
-
-
Save tylergaw/909466efa08f4cdb74dd68e0f9dc9e69 to your computer and use it in GitHub Desktop.
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
const CATCH_ALL_ERR_MSG = | |
"We weren’t able to determine what the error was based on the server response."; | |
/** | |
* window.fetch does not throw an error for common HTTP errors; 404, 503, etc. | |
* Here we check its `ok` key to determine if there's n HTTP error. | |
* If so, throw an error so any users can `catch`. | |
* | |
* @param {Object} res - A Response object from fetch | |
*/ | |
const throwOnError = async res => { | |
const codeToText = { | |
"401": | |
"Based on your authentication, you don’t have access to this content.", | |
"404": "Not found" | |
}; | |
if (!res.ok) { | |
let json = {}; | |
// Make sure the response is application/json before attempting .json() | |
// because not doing so results in another error if it's not json. | |
if (responseIsJSON(res)) { | |
json = await res.json(); | |
} | |
const statusText = | |
res.statusText || | |
codeToText[res.status] || | |
json.message || | |
json.error || | |
CATCH_ALL_ERR_MSG; | |
throw Error( | |
JSON.stringify({ | |
status: res.status, | |
statusText | |
}) | |
); | |
} | |
return res; | |
}; | |
/** | |
* Quick check to see if a response is json based on the content-type header. | |
* @param {Response} res - A valid Response object. | |
* @return {Boolean} | |
*/ | |
const responseIsJSON = res => { | |
const contentType = res.headers.get("content-type"); | |
// If the repsonse didn't even have the necessary header set, just fail. | |
if (!contentType) { | |
return false; | |
} | |
return contentType.includes("application/json"); | |
}; | |
/** | |
* createFetchWriteFunc - Internal fetch POST|PUT|PATCH function creator. | |
* We want to provide a useful external api with http.post, http.put, et al. | |
* Since each of those are the same function body, we use this to create those. | |
* functions. | |
* | |
* @param {String} method - The HTTP method. | |
* @return {Function} | |
*/ | |
const createFetchWriteFunc = method => async ( | |
url, | |
body, | |
headers = {}, | |
settings = {} | |
) => { | |
const res = await fetch(url, { | |
method, | |
body: JSON.stringify(body), | |
headers: new Headers({ | |
Accept: "application/json", | |
"Content-Type": "application/json", | |
...headers | |
}), | |
...settings | |
}); | |
// Send a clone so we don't try to double-read the response. | |
try { | |
await throwOnError(res.clone()); | |
} catch (err) { | |
throw err; | |
} | |
// If we make it this far, the response is OK, 2xx status. We'd like to get | |
// JSON, but if the response doesn't have it just return part of the response | |
// and let the consumer handle it as needed. | |
if (!responseIsJSON(res)) { | |
return { status: res.status }; | |
} else { | |
return await res.json(); | |
} | |
}; | |
/** | |
* get - A light facade around window.fetch. | |
* | |
* @param {String} url | |
* @return {JSON|Error} | |
*/ | |
export const get = async (url, params = {}, settings = {}) => { | |
const res = await fetch(`${url}?${objToQueryStr(params)}`, { | |
...settings | |
}); | |
// Send a clone so we don't try to double-read the response. | |
try { | |
await throwOnError(res.clone()); | |
} catch (err) { | |
throw err; | |
} | |
if (!responseIsJSON(res)) { | |
throw Error( | |
JSON.stringify({ | |
status: null, | |
statusText: | |
"Received a valid response from the server, but it’s not JSON so we don’t know how to display it." | |
}) | |
); | |
} | |
return await res.json(); | |
}; | |
/** | |
* http.post - A light facade around window.fetch for POST-ing | |
* | |
* @param {String} url | |
* @param {Object} body - A JSON.stringify-able object. | |
* @return {JSON|Error} | |
*/ | |
export const post = createFetchWriteFunc("POST"); | |
/** | |
* http.put - A light facade around window.fetch for PUT-ing | |
* | |
* @param {String} url | |
* @param {Object} body - A JSON.stringify-able object. | |
* @return {JSON|Error} | |
*/ | |
export const put = createFetchWriteFunc("PUT"); | |
// TODO: Remove filename when content-disposition header issue is resolved. | |
export const download = async (url, filename, settings = {}) => { | |
const res = await fetch(url, { | |
...settings | |
}); | |
const blob = await res.blob(); | |
return new Promise((resolve, reject) => { | |
try { | |
// FIXME: This seems suspect here | |
const a = document.createElement("a"); | |
document.body.appendChild(a); | |
a.style = "display: none"; | |
const href = window.URL.createObjectURL(blob); | |
a.href = href; | |
a.download = filename; | |
a.click(); | |
window.URL.revokeObjectURL(href); | |
resolve({ downloaded: true }); | |
} catch (err) { | |
reject(err); | |
} | |
}); | |
}; | |
/** | |
* http.del - A light facade around window.fetch for DELETE-ing | |
* | |
* @param {String} url | |
* @param {Object} body - A JSON.stringify-able object. | |
* @return {JSON|Error} | |
*/ | |
export const del = createFetchWriteFunc("DELETE"); | |
/** | |
* Given an error from a failed fetch request, return a standard JSON object. | |
* @param {Object|String} - Either a JSON string or regular string. | |
* @return {Object} | |
*/ | |
export const getJSONFromErr = err => { | |
let errJSON = {}; | |
try { | |
errJSON = JSON.parse(err.message); | |
} catch (e) { | |
errJSON = { | |
status: 0, | |
statusText: err.message | |
}; | |
} | |
// If we get here and don't have a statusText message, at least let the user know | |
// we're not sure what happened. | |
if (!errJSON.statusText) { | |
errJSON.statusText = CATCH_ALL_ERR_MSG; | |
} | |
return errJSON; | |
}; | |
/** | |
* Convert string of query params "?foo=bar&fish=dogs&dogs=cool" to key/val obj | |
* | |
* @param {String} str - A string from window.location.search | |
* @return {Object} | |
*/ | |
export const queryStrToObj = str => { | |
try { | |
return JSON.parse( | |
'{"' + | |
decodeURI(str.substring(1)) | |
.replace(/"/g, '\\"') | |
.replace(/&/g, '","') | |
.replace(/=/g, '":"') + | |
'"}' | |
); | |
} catch (err) { | |
return {}; | |
} | |
}; | |
/** | |
* Convert an obj of key/vals to query string "foo=bar&fish=dogs&dogs=cool" | |
* | |
* @param {Object} obj - key/value pairs | |
* @return {String} | |
*/ | |
export const objToQueryStr = obj => | |
Object.keys(obj) | |
.map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`) | |
.join("&"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment