Home Reference Source Test Repository

src/util/fetch-util.js

import Logger from './logger';
import NPROneSDK from './../index';
import ApiError from './../error/api-error';


/**
 * Simulates a delay by wrapping a Promise around JavaScript's native `setTimeout` function.
 *
 * @param {number} ms The amount of time to delay for, in milliseconds
 * @returns {Promise}
 * @private
 */
const delay = ms => new Promise(r => setTimeout(r, ms));


/**
 * A thin wrapper around the Fetch API which provides functionality to automatically
 * request a new access token if an existing one has expired.
 */
export default class FetchUtil
{
    /**
     * Primary workhorse for interacting with the NPR One APIs.
     *
     * @param {string} url
     * @param {Object} [options]
     * @returns {Promise<Object>}
     */
    static nprApiFetch(url, options = {}) {
        Logger.debug(`Starting JSON fetch ${url}`);

        if (!FetchUtil._requestUrlIsAuthorizationCall(url) && !options.headers) {
            options.headers = FetchUtil._getHeaders(); // eslint-disable-line
        }

        return fetch(url, options)
            .then((response) => {
                if (response.ok) {
                    return response.json();
                } else if (response.status === 401 && Boolean(NPROneSDK.accessToken)
                    && !FetchUtil._requestUrlIsAuthorizationCall(url)) {
                    return FetchUtil._attemptAccessTokenRefresh(url, options);
                }
                return FetchUtil.formatErrorResponse(response);
            });
    }

    /**
     * Wraps an error response from an API call in an {@link ApiError} object so that consuming code has more
     * flexibility to determine how to handle the error. To be clear: this function returns a Promise that **always**
     * rejects, but it may or may not have the deserialized JSON body based on whether `response.json()` succeeded or
     * failed (the latter is usually an indicator that the response had an empty body).
     *
     * @param {Response} response
     * @returns {Promise}
     */
    static formatErrorResponse(response) {
        return response.json()
            .then((json) => {
                throw new ApiError(response, json);
            }, (err) => { // this will ONLY catch errors from the deserialization, and not from the line above this
                Logger.debug('Problem deserializing JSON from API error');
                Logger.debug(err);
                throw new ApiError(response);
            });
    }

    /**
     * The logic to attempt an access token refresh, broken out for easier readability.
     *
     * @param {string} url
     * @param {Object} options
     * @returns {Promise<Object>}
     * @private
     */
    static _attemptAccessTokenRefresh(url, options) {
        return NPROneSDK.refreshExistingAccessToken()
            .then(() => { // retry the original request we were making, after a short delay
                const _options = options;
                /* istanbul ignore else */ // defensive coding
                if (options.headers) {
                    _options.headers = FetchUtil._getHeaders(); // make sure we use the new access token
                }

                return delay(250)
                    .then(FetchUtil.nprApiFetch.bind(this, url, _options));
            });
    }

    /**
     * Ensures access token is defined and generates the required Headers object for fetch
     *
     * @returns {Headers}
     * @private
     */
    static _getHeaders() {
        if (NPROneSDK.accessToken === '') {
            throw new Error('An Access Token must set before making API requests.');
        }

        const headers = new Headers();
        headers.append('Authorization', `Bearer ${NPROneSDK.accessToken}`);
        if (NPROneSDK.config.advertisingId) {
            headers.append('X-Advertising-ID', `${NPROneSDK.config.advertisingId}`);
        }
        if (NPROneSDK.config.advertisingTarget) {
            headers.append('X-Advertising-Target', `${NPROneSDK.config.advertisingTarget}`);
        }
        return headers;
    }

    /**
     * Tests whether or not the call to the given URL should be considered an authorization call.
     *
     * @param {string} url
     * @returns {boolean}
     * @private
     */
    static _requestUrlIsAuthorizationCall(url) {
        return (NPROneSDK.config.authProxyBaseUrl && (url.indexOf(NPROneSDK.config.authProxyBaseUrl) > -1))
            || new RegExp(NPROneSDK.getServiceUrl('authorization')).test(url);
    }
}