HttpClient.js

class HTTPError extends Error {
    constructor(code, statusText, message, response) {
        super(message || statusText);
        if (arguments.length >= 4 && response) {
            Object.assign(this, response);
        }
        this.statusText = statusText;
        this.statusCode = code;
        this.response = response;
    }
}

/**
 * Wrapper for fetch.
 * Inspired by https://kentcdodds.com/blog/replace-axios-with-a-simple-custom-fetch-wrapper
 *
 * @param {String} url The url of the ressource to fetch.
 * @param {Object=} json Optional. Data used as json-serialized payload.
 * @param {String=} method Optional. The HTTP method, e.g. POST. Default: GET (if json is unset).
 * @param {Object=} params Optional. Params used as body for POST and get-query-String for others.
 * @param {Object=} customConfig Some custom values for the fetch call.
 * @returns {Promise<Response>} Returns the raw request a promise, or throws a HTTPError.
 */
function request(url, {json, method, params, ...customConfig} = {}) {
    if (!method) {
        method = json ? 'POST' : 'GET'
    }
    const config = {
        credentials: 'include',
        method: method,
        headers: {
            ...customConfig.headers,
        },
        ...customConfig,
    }
    if (url.startsWith('/') || url.startsWith(window.location.origin)) {
        // TODO: Additional headers causes CORS problems
        //  and preflight OPTION requests are not working with the current viur-core
        config.headers['X-Requested-With'] = 'Fetch';
    }
    if (json) {
        config.body = JSON.stringify(json);
        config.headers['Content-Type'] = 'application/json';
    } else if (params && method === 'POST') {
        if (typeof params === 'string' || params instanceof String) {
            // provided a query string, e.g.: foo=1&bar=2
            config.body = new URLSearchParams(params);
        } else if (params instanceof HTMLFormElement) {
            // provided a HTML form element
            config.body = new FormData(params);
        } else {
            // provided a object, e.g.: {foo: 1, bar: 2, baz: [3, 4]}
            config.body = new FormData();
            Object.entries(params).forEach(([key, value]) => {
                if (Array.isArray(value)) {
                    for (const val of value) {
                        config.body.append(key, val)
                    }
                } else {
                    config.body.append(key, value)
                }
            });
        }
    }
    if (params && (method === 'GET' || json)) {
        const getParams = new URLSearchParams(params)
        url += `?${getParams.toString()}`;
    }
    return window
        .fetch(url, config)
        .then(async response => {
            if (response.ok || (config.mode === 'no-cors' && response.type === 'opaque')) {
                return response
            } else {
                const errorMessage = `${response.status} ${response.statusText}: ${response.headers.get('x-viur-error')}`
                return Promise.reject(new HTTPError(response.status, response.statusText, errorMessage, response))
            }
        })
}

/**
 * Return a new skey.
 * @returns {Promise<String>} The resolved json-Promise (the skey).
 */
function getSkey() {
    return request('/json/skey')
        .then(request => request.json())
}

/**
 * Shows a alert with an error-message.
 * @param {any} error The exception. Could be an instance of HTTPError or any other error/value.
 */
function errorHandler(error) {
    console.error(error);
    let headline = `Error`;
    if (error instanceof HTTPError) {
        headline += ` ${error.statusCode} ${error.statusText}`;
    }
    alert(
        `Sorry, an unexpected error occurred.
${headline}
${error}
Please try again our contact our customer service.`,
    )
}

export {HTTPError, request, getSkey, errorHandler};