Make a request to the REST API.

This is a wrapper around {@link http://api.jquery.com/jQuery.ajax/ $.ajax}, which also handles
authentication and routing to the Girder API, and provides a default for error notification.

Most users of this method should attach a "done" handler to the return value. In cases where the
server is ordinarily expected to return a non-200 status (e.g. validating user input), the
"error: null" argument should probably be provided, and errors should be processed via an
attached "fail" handler.

Before this function is called, the API root must be set (which typically happens automatically).

@param {Object} opts Options for the request, most of which will be passed through to $.ajax.
@param {string} opts.url The resource path, relative to the API root, without leading or trailing
slashes. e.g. "user/login"
@param {string} [opts.method='GET'] The HTTP method to invoke.
@param {string|Object} [opts.data] The query string or form parameter object.
@param {?Function} [opts.error] An error callback, as documented in
{@link http://api.jquery.com/jQuery.ajax/ $.ajax}, or null. If not provided, this will
have a default behavior of triggering a 'g:alert' global event, with details of the
error, and logging the error to the console. It is recommended that you do not ever pass
a non-null callback function, and handle errors via promise rejection handlers instead.
@param {string} [opts.girderToken] An alternative auth token to use for this request.
@returns {$.Promise} A jqXHR promise, which resolves and rejects
{@link http://api.jquery.com/jQuery.ajax/#jqXHR as documented by $.ajax}.
function (opts) {
    opts = opts || {};
    const defaults = {
        // the default 'method' is 'GET', as set by 'jquery.ajax'

        girderToken: getCurrentToken() || cookie.find('girderToken'),

        error: (error, status) => {
            let info;
            if (error.status === 401) {
                events.trigger('g:loginUi');
                info = {
                    text: 'You must log in to view this resource',
                    type: 'warning',
                    timeout: 4000,
                    icon: 'info'
                };
            } else if (error.status === 403) {
                info = {
                    text: 'Access denied. See the console for more details.',
                    type: 'danger',
                    timeout: 5000,
                    icon: 'attention'
                };
            } else if (error.status === 0 && error.statusText === 'abort') {
                /* We expected this abort, so do nothing. */
                return;
            } else if (error.status === 500 && error.responseJSON &&
                error.responseJSON.type === 'girder') {
                info = {
                    text: error.responseJSON.message,
                    type: 'warning',
                    timeout: 5000,
                    icon: 'info'
                };
            } else if (status === 'parsererror') {
                info = {
                    text: 'A parser error occurred while communicating with the ' +
                          'server (did you use the correct value for `dataType`?). ' +
                          'Details have been logged in the console.',
                    type: 'danger',
                    timeout: 5000,
                    icon: 'attention'
                };
            } else {
                info = {
                    text: 'An error occurred while communicating with the ' +
                          'server. Details have been logged in the console.',
                    type: 'danger',
                    timeout: 5000,
                    icon: 'attention'
                };
            }
            events.trigger('g:alert', info);
            console.error(error.status + ' ' + error.statusText, error.responseText);
        }
    };

    // Overwrite defaults with passed opts, but do not mutate opts
    const args = _.extend({}, defaults, opts);

    if (!args.url) {
        throw new Error('restRequest requires a "url" argument');
    }
    args.url = `${getApiRoot()}${args.url.substring(0, 1) === '/' ? '' : '/'}${args.url}`;

    if (args.girderToken) {
        args.headers = args.headers || {};
        args.headers['Girder-Token'] = args.girderToken;
        delete args.girderToken;
    }

    return Backbone.$.ajax(args);
}