import 'isomorphic-unfetch';
import HttpError from './HttpError';
import NetworkError from './NetworkError';
import ValidResponse from './ValidResponse';
/**
* @module request
*/
/**
* Gets the body of a response. If the response has a `Content-Type` header set to `application/json`, it will parse the body as JSON and return the result.
*
* NOTE: running this function will lock the response body. It will be impossible to re-read the response. **It should only ever be run on a `Response` object one time.**
*
* @param {Response} response a `fetch` `Response` object
* @memberof module:request
* @private
*/
const getBody = async (response) => {
const contentType = response.headers.get('Content-Type');
let body;
if (contentType && contentType.includes('application/json')) {
body = await response.json();
} else {
body = await response.text();
}
return body;
};
/**
* Derives a url string to use in `fetch`
*
* @param {String} urlArg - the url
* @param {String} method - the method to use (GET, POST, etc.)
* @param {Object|String} body - the content to send
* @private
*/
const getUrl = (urlArg, method, body = {}) => {
const url = new URL(urlArg);
if (method.toLowerCase() === 'get') {
Object.entries(body).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
return url.toString();
};
/**
* Creates an error object from the response.
*
* @param {Response} response a `fetch` `Response` object
* @return {module:request.HttpError} the error object derived from the `response`.
* @memberof module:request
* @private
*/
const createError = async (response) => {
const body = await getBody(response);
return new HttpError(
response,
body,
`The server responded with HTTP error code ${response.status}`,
);
};
/**
* Creates a response object with the following keys:
*
* @param {Response} response a `fetch` `Response` object
* @return {module:request.ValidResponse}
* @memberof module:request
* @private
*/
const createResponse = async (response) => {
const body = await getBody(response);
return new ValidResponse(response, body);
};
/**
* Derives an options object to use on `fetch`, given its parameters. Handles things like `Content-Type` headers, methods, and body encoding.
*
* @param {String} method - the method to use (GET, POST, etc.)
* @param {Object|String} body - the content to send
* @param {Object} opts - the default options to use
* @private
*/
const getOptions = (method, body, opts) => {
const sendableOptions = { headers: {}, method, ...opts };
if (typeof body === 'undefined' || method.toLowerCase() === 'get') {
return sendableOptions;
}
const isJson = typeof body !== 'string';
const contentType = isJson ? 'application/json' : 'text/plain';
sendableOptions.body = isJson ? JSON.stringify(body) : body;
if (!sendableOptions.headers['Content-Type']) {
sendableOptions.headers['Content-Type'] = contentType;
}
return sendableOptions;
};
/**
* Make a request to a URL.
*
* @param {string} url the URL to send a request to.
* @param {String} [method="GET"] the HTTP method to use (GET, POST, PUT, DELETE)
* @param {Object|String} [body=undefined] the content of the body of the request. If it's an object, will automatically apply `JSON.stringify`; otherwise, will send it as a string. Use `undefined` to specify no message body.
* @param {*} [opts={}] `fetch` options. See `fetch()` init param: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
* @throws {module:request.HttpError}
*
* If the response contains an HTTP Status Code in the error range (4xx or 5xx), then `request()` will throw an `HttpError`. This can be useful to detect, for example, a `401 Unauthorized`, a `404 Not Found`, a `500 Internal Server Error`, or any other HTTP status code.
*
* This error type can be detected by checking if `error instanceof HttpError`.
* @throws {module:request.NetworkError}
*
* In the event of a network error, a `NetworkError` will throw. This can happen if:
* - the user disconnects from their WiFi network
* - the servers are down
* - the request times out
*
* In the real world, this can happen if a user travels through a tunnel, disconnecting them from their mobile network. It's an important edge case to account for.
*
* This error type can be detected by checking if `error instanceof NetworkError`.
* @return {Promise<module:request.ValidResponse>} a promise, resolving to a valid, non-error response object.
* @memberof module:request
*/
const request = async (urlArg, method = 'GET', body = undefined, opts = {}) => {
const sendableOptions = getOptions(method, body, opts);
const url = getUrl(urlArg, method, body);
let response;
try {
response = await fetch(url, sendableOptions);
} catch (error) {
throw new NetworkError(
error,
{ url, ...sendableOptions },
'A network error occurred. The network connection may have been disconnected, or the service may be down.',
);
}
if (!response.ok) {
throw await createError(response);
}
return createResponse(response);
};
export default request;