Skip to content

Instantly share code, notes, and snippets.

@tylergaw
Created April 22, 2020 12:31
Show Gist options
  • Save tylergaw/909466efa08f4cdb74dd68e0f9dc9e69 to your computer and use it in GitHub Desktop.
Save tylergaw/909466efa08f4cdb74dd68e0f9dc9e69 to your computer and use it in GitHub Desktop.
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