Revert "Refactor gr-rest-api-interface - extract common methods to helper"

This reverts commit e2d84b21ad953e48cc9a7c702da78fd766c1b4da.

Reason for revert: document.createElement('gr-rest-api-interface').send(...) doesn't work in Polymer 2, because the ready() method of gr-rest-api-interface calls later. 

Change-Id: I42234c2e2e6801b144e4d1db33e9d7929ace219e
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 7461ac4..87ea02b 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -29,7 +29,6 @@
 
 <dom-module id="gr-rest-api-interface">
   <!-- NB: Order is important, because of namespaced classes. -->
-  <script src="gr-rest-apis/gr-rest-api-helper.js"></script>
   <script src="gr-auth.js"></script>
   <script src="gr-reviewer-updates-parser.js"></script>
   <script src="gr-rest-api-interface.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index d634f14..9b63f75 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -29,6 +29,34 @@
 
   /**
    * @typedef {{
+   *    url: string,
+   *    fetchOptions: (Object|null|undefined),
+   *    anonymizedUrl: (string|undefined),
+   * }}
+   */
+  Defs.FetchRequest;
+
+  /**
+   * Object to describe a request for passing into _fetchJSON or _fetchRawJSON.
+   * - url is the URL for the request (excluding get params)
+   * - errFn is a function to invoke when the request fails.
+   * - cancelCondition is a function that, if provided and returns true, will
+   *     cancel the response after it resolves.
+   * - params is a key-value hash to specify get params for the request URL.
+   * @typedef {{
+   *    url: string,
+   *    errFn: (function(?Response, string=)|null|undefined),
+   *    cancelCondition: (function()|null|undefined),
+   *    params: (Object|null|undefined),
+   *    fetchOptions: (Object|null|undefined),
+   *    anonymizedUrl: (string|undefined),
+   *    reportUrlAsIs: (boolean|undefined),
+   * }}
+   */
+  Defs.FetchJSONRequest;
+
+  /**
+   * @typedef {{
    *   changeNum: (string|number),
    *   endpoint: string,
    *   patchNum: (string|number|null|undefined),
@@ -93,6 +121,7 @@
   const MAX_PROJECT_RESULTS = 25;
   const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900;
   const PARENT_PATCH_NUM = 'PARENT';
+  const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
 
   const Requests = {
     SEND_DIFF_DRAFT: 'sendDiffDraft',
@@ -106,6 +135,60 @@
   const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL +
       '/revisions/*';
 
+  /**
+   * Wrapper around Map for caching server responses. Site-based so that
+   * changes to CANONICAL_PATH will result in a different cache going into
+   * effect.
+   */
+  class SiteBasedCache {
+    constructor() {
+      // Container of per-canonical-path caches.
+      this._data = new Map();
+      if (window.INITIAL_DATA != undefined) {
+        // Put all data shipped with index.html into the cache. This makes it
+        // so that we spare more round trips to the server when the app loads
+        // initially.
+        Object
+            .entries(window.INITIAL_DATA)
+            .forEach(e => this._cache().set(e[0], e[1]));
+      }
+    }
+
+    // Returns the cache for the current canonical path.
+    _cache() {
+      if (!this._data.has(window.CANONICAL_PATH)) {
+        this._data.set(window.CANONICAL_PATH, new Map());
+      }
+      return this._data.get(window.CANONICAL_PATH);
+    }
+
+    has(key) {
+      return this._cache().has(key);
+    }
+
+    get(key) {
+      return this._cache().get(key);
+    }
+
+    set(key, value) {
+      this._cache().set(key, value);
+    }
+
+    delete(key) {
+      this._cache().delete(key);
+    }
+
+    invalidatePrefix(prefix) {
+      const newMap = new Map();
+      for (const [key, value] of this._cache().entries()) {
+        if (!key.startsWith(prefix)) {
+          newMap.set(key, value);
+        }
+      }
+      this._data.set(window.CANONICAL_PATH, newMap);
+    }
+  }
+
   Polymer({
     is: 'gr-rest-api-interface',
     _legacyUndefinedCheck: true,
@@ -152,7 +235,7 @@
       },
       _sharedFetchPromises: {
         type: Object,
-        value: new FetchPromisesCache(), // Shared across instances.
+        value: {}, // Intentional to share the object across instances.
       },
       _pendingRequests: {
         type: Object,
@@ -176,14 +259,133 @@
     },
 
     JSON_PREFIX,
-    ready() {
-      this._restApiHelper = new GrRestApiHelper(this._cache, this._auth,
-          this._sharedFetchPromises, this._credentialCheck, this);
+
+    /**
+     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
+     * with timing and logging.
+     * @param {Defs.FetchRequest} req
+     */
+    _fetch(req) {
+      const start = Date.now();
+      const xhr = this._auth.fetch(req.url, req.fetchOptions);
+
+      // Log the call after it completes.
+      xhr.then(res => this._logCall(req, start, res.status));
+
+      // Return the XHR directly (without the log).
+      return xhr;
     },
 
-    _fetchSharedCacheURL(req) {
-      // Cache is shared across instances
-      return this._restApiHelper.fetchCacheURL(req);
+    /**
+     * Log information about a REST call. Because the elapsed time is determined
+     * by this method, it should be called immediately after the request
+     * finishes.
+     * @param {Defs.FetchRequest} req
+     * @param {number} startTime the time that the request was started.
+     * @param {number} status the HTTP status of the response. The status value
+     *     is used here rather than the response object so there is no way this
+     *     method can read the body stream.
+     */
+    _logCall(req, startTime, status) {
+      const method = (req.fetchOptions && req.fetchOptions.method) ?
+          req.fetchOptions.method : 'GET';
+      const endTime = Date.now();
+      const elapsed = (endTime - startTime);
+      const startAt = new Date(startTime);
+      const endAt = new Date(endTime);
+      console.log([
+        'HTTP',
+        status,
+        method,
+        elapsed + 'ms',
+        req.anonymizedUrl || req.url,
+        `(${startAt.toISOString()}, ${endAt.toISOString()})`,
+      ].join(' '));
+      if (req.anonymizedUrl) {
+        this.fire('rpc-log',
+            {status, method, elapsed, anonymizedUrl: req.anonymizedUrl});
+      }
+    },
+
+    /**
+     * Fetch JSON from url provided.
+     * Returns a Promise that resolves to a native Response.
+     * Doesn't do error checking. Supports cancel condition. Performs auth.
+     * Validates auth expiry errors.
+     * @param {Defs.FetchJSONRequest} req
+     */
+    _fetchRawJSON(req) {
+      const urlWithParams = this._urlWithParams(req.url, req.params);
+      const fetchReq = {
+        url: urlWithParams,
+        fetchOptions: req.fetchOptions,
+        anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
+      };
+      return this._fetch(fetchReq).then(res => {
+        if (req.cancelCondition && req.cancelCondition()) {
+          res.body.cancel();
+          return;
+        }
+        return res;
+      }).catch(err => {
+        const isLoggedIn = !!this._cache.get('/accounts/self/detail');
+        if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
+          this.checkCredentials();
+        } else {
+          if (req.errFn) {
+            req.errFn.call(undefined, null, err);
+          } else {
+            this.fire('network-error', {error: err});
+          }
+        }
+        throw err;
+      });
+    },
+
+    /**
+     * Fetch JSON from url provided.
+     * Returns a Promise that resolves to a parsed response.
+     * Same as {@link _fetchRawJSON}, plus error handling.
+     * @param {Defs.FetchJSONRequest} req
+     */
+    _fetchJSON(req) {
+      req = this._addAcceptJsonHeader(req);
+      return this._fetchRawJSON(req).then(response => {
+        if (!response) {
+          return;
+        }
+        if (!response.ok) {
+          if (req.errFn) {
+            req.errFn.call(null, response);
+            return;
+          }
+          this.fire('server-error', {request: req, response});
+          return;
+        }
+        return response && this.getResponseObject(response);
+      });
+    },
+
+    /**
+     * @param {string} url
+     * @param {?Object|string=} opt_params URL params, key-value hash.
+     * @return {string}
+     */
+    _urlWithParams(url, opt_params) {
+      if (!opt_params) { return this.getBaseUrl() + url; }
+
+      const params = [];
+      for (const p in opt_params) {
+        if (!opt_params.hasOwnProperty(p)) { continue; }
+        if (opt_params[p] == null) {
+          params.push(encodeURIComponent(p));
+          continue;
+        }
+        for (const value of [].concat(opt_params[p])) {
+          params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
+        }
+      }
+      return this.getBaseUrl() + url + '?' + params.join('&');
     },
 
     /**
@@ -191,7 +393,45 @@
      * @return {?}
      */
     getResponseObject(response) {
-      return this._restApiHelper.getResponseObject(response);
+      return this._readResponsePayload(response)
+          .then(payload => payload.parsed);
+    },
+
+    /**
+     * @param {!Object} response
+     * @return {!Object}
+     */
+    _readResponsePayload(response) {
+      return response.text().then(text => {
+        let result;
+        try {
+          result = this._parsePrefixedJSON(text);
+        } catch (_) {
+          result = null;
+        }
+        return {parsed: result, raw: text};
+      });
+    },
+
+    /**
+     * @param {string} source
+     * @return {?}
+     */
+    _parsePrefixedJSON(source) {
+      return JSON.parse(source.substring(JSON_PREFIX.length));
+    },
+
+    /**
+     * @param {Defs.FetchJSONRequest} req
+     * @return {Defs.FetchJSONRequest}
+     */
+    _addAcceptJsonHeader(req) {
+      if (!req.fetchOptions) req.fetchOptions = {};
+      if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
+      if (!req.fetchOptions.headers.has('Accept')) {
+        req.fetchOptions.headers.append('Accept', 'application/json');
+      }
+      return req;
     },
 
     getConfig(noCache) {
@@ -202,7 +442,7 @@
         });
       }
 
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/config/server/info',
         reportUrlAsIs: true,
       });
@@ -252,7 +492,7 @@
       // supports it.
       const url = `/projects/${encodeURIComponent(repo)}/config`;
       this._cache.delete(url);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url,
         body: config,
@@ -266,7 +506,7 @@
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
       const encodeName = encodeURIComponent(repo);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'POST',
         url: `/projects/${encodeName}/gc`,
         body: '',
@@ -284,7 +524,7 @@
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
       const encodeName = encodeURIComponent(config.name);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/projects/${encodeName}`,
         body: config,
@@ -300,7 +540,7 @@
     createGroup(config, opt_errFn) {
       if (!config.name) { return ''; }
       const encodeName = encodeURIComponent(config.name);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/groups/${encodeName}`,
         body: config,
@@ -310,7 +550,7 @@
     },
 
     getGroupConfig(group, opt_errFn) {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: `/groups/${encodeURIComponent(group)}/detail`,
         errFn: opt_errFn,
         anonymizedUrl: '/groups/*/detail',
@@ -328,7 +568,7 @@
       // supports it.
       const encodeName = encodeURIComponent(repo);
       const encodeRef = encodeURIComponent(ref);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: `/projects/${encodeName}/branches/${encodeRef}`,
         body: '',
@@ -348,7 +588,7 @@
       // supports it.
       const encodeName = encodeURIComponent(repo);
       const encodeRef = encodeURIComponent(ref);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: `/projects/${encodeName}/tags/${encodeRef}`,
         body: '',
@@ -369,7 +609,7 @@
       // supports it.
       const encodeName = encodeURIComponent(name);
       const encodeBranch = encodeURIComponent(branch);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/projects/${encodeName}/branches/${encodeBranch}`,
         body: revision,
@@ -390,7 +630,7 @@
       // supports it.
       const encodeName = encodeURIComponent(name);
       const encodeTag = encodeURIComponent(tag);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/projects/${encodeName}/tags/${encodeTag}`,
         body: revision,
@@ -415,7 +655,7 @@
 
     getGroupMembers(groupName, opt_errFn) {
       const encodeName = encodeURIComponent(groupName);
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: `/groups/${encodeName}/members/`,
         errFn: opt_errFn,
         anonymizedUrl: '/groups/*/members',
@@ -423,7 +663,7 @@
     },
 
     getIncludedGroup(groupName) {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: `/groups/${encodeURIComponent(groupName)}/groups/`,
         anonymizedUrl: '/groups/*/groups',
       });
@@ -431,7 +671,7 @@
 
     saveGroupName(groupId, name) {
       const encodeId = encodeURIComponent(groupId);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/groups/${encodeId}/name`,
         body: {name},
@@ -441,7 +681,7 @@
 
     saveGroupOwner(groupId, ownerId) {
       const encodeId = encodeURIComponent(groupId);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/groups/${encodeId}/owner`,
         body: {owner: ownerId},
@@ -451,7 +691,7 @@
 
     saveGroupDescription(groupId, description) {
       const encodeId = encodeURIComponent(groupId);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/groups/${encodeId}/description`,
         body: {description},
@@ -461,7 +701,7 @@
 
     saveGroupOptions(groupId, options) {
       const encodeId = encodeURIComponent(groupId);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/groups/${encodeId}/options`,
         body: options,
@@ -480,7 +720,7 @@
     saveGroupMembers(groupName, groupMembers) {
       const encodeName = encodeURIComponent(groupName);
       const encodeMember = encodeURIComponent(groupMembers);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/groups/${encodeName}/members/${encodeMember}`,
         parseResponse: true,
@@ -497,7 +737,7 @@
         errFn: opt_errFn,
         anonymizedUrl: '/groups/*/groups/*',
       };
-      return this._restApiHelper.send(req).then(response => {
+      return this._send(req).then(response => {
         if (response.ok) {
           return this.getResponseObject(response);
         }
@@ -507,7 +747,7 @@
     deleteGroupMembers(groupName, groupMembers) {
       const encodeName = encodeURIComponent(groupName);
       const encodeMember = encodeURIComponent(groupMembers);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: `/groups/${encodeName}/members/${encodeMember}`,
         anonymizedUrl: '/groups/*/members/*',
@@ -517,7 +757,7 @@
     deleteIncludedGroup(groupName, includedGroup) {
       const encodeName = encodeURIComponent(groupName);
       const encodeIncludedGroup = encodeURIComponent(includedGroup);
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`,
         anonymizedUrl: '/groups/*/groups/*',
@@ -604,7 +844,7 @@
         prefs.download_scheme = prefs.download_scheme.toLowerCase();
       }
 
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: '/accounts/self/preferences',
         body: prefs,
@@ -620,7 +860,7 @@
     saveDiffPreferences(prefs, opt_errFn) {
       // Invalidate the cache.
       this._cache.delete('/accounts/self/preferences.diff');
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: '/accounts/self/preferences.diff',
         body: prefs,
@@ -636,7 +876,7 @@
     saveEditPreferences(prefs, opt_errFn) {
       // Invalidate the cache.
       this._cache.delete('/accounts/self/preferences.edit');
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: '/accounts/self/preferences.edit',
         body: prefs,
@@ -670,14 +910,14 @@
     },
 
     getExternalIds() {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/accounts/self/external.ids',
         reportUrlAsIs: true,
       });
     },
 
     deleteAccountIdentity(id) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'POST',
         url: '/accounts/self/external.ids:delete',
         body: id,
@@ -691,7 +931,7 @@
      * @return {!Promise<!Object>}
      */
     getAccountDetails(userId) {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: `/accounts/${encodeURIComponent(userId)}/detail`,
         anonymizedUrl: '/accounts/*/detail',
       });
@@ -709,7 +949,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     addAccountEmail(email, opt_errFn) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: '/accounts/self/emails/' + encodeURIComponent(email),
         errFn: opt_errFn,
@@ -722,7 +962,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     deleteAccountEmail(email, opt_errFn) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: '/accounts/self/emails/' + encodeURIComponent(email),
         errFn: opt_errFn,
@@ -742,7 +982,7 @@
         errFn: opt_errFn,
         anonymizedUrl: '/accounts/self/emails/*/preferred',
       };
-      return this._restApiHelper.send(req).then(() => {
+      return this._send(req).then(() => {
         // If result of getAccountEmails is in cache, update it in the cache
         // so we don't have to invalidate it.
         const cachedEmails = this._cache.get('/accounts/self/emails');
@@ -786,7 +1026,7 @@
         parseResponse: true,
         reportUrlAsIs: true,
       };
-      return this._restApiHelper.send(req)
+      return this._send(req)
           .then(newName => this._updateCachedAccount({name: newName}));
     },
 
@@ -803,7 +1043,7 @@
         parseResponse: true,
         reportUrlAsIs: true,
       };
-      return this._restApiHelper.send(req)
+      return this._send(req)
           .then(newName => this._updateCachedAccount({username: newName}));
     },
 
@@ -820,33 +1060,33 @@
         parseResponse: true,
         reportUrlAsIs: true,
       };
-      return this._restApiHelper.send(req)
+      return this._send(req)
           .then(newStatus => this._updateCachedAccount({status: newStatus}));
     },
 
     getAccountStatus(userId) {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: `/accounts/${encodeURIComponent(userId)}/status`,
         anonymizedUrl: '/accounts/*/status',
       });
     },
 
     getAccountGroups() {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/accounts/self/groups',
         reportUrlAsIs: true,
       });
     },
 
     getAccountAgreements() {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/accounts/self/agreements',
         reportUrlAsIs: true,
       });
     },
 
     saveAccountAgreement(name) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: '/accounts/self/agreements',
         body: name,
@@ -891,7 +1131,34 @@
     },
 
     checkCredentials() {
-      return this._restApiHelper.checkCredentials();
+      if (this._credentialCheck.checking) {
+        return;
+      }
+      this._credentialCheck.checking = true;
+      let req = {url: '/accounts/self/detail', reportUrlAsIs: true};
+      req = this._addAcceptJsonHeader(req);
+      // Skip the REST response cache.
+      return this._fetchRawJSON(req).then(res => {
+        if (!res) { return; }
+        if (res.status === 403) {
+          this.fire('auth-error');
+          this._cache.delete('/accounts/self/detail');
+        } else if (res.ok) {
+          return this.getResponseObject(res);
+        }
+      }).then(res => {
+        this._credentialCheck.checking = false;
+        if (res) {
+          this._cache.set('/accounts/self/detail', res);
+        }
+        return res;
+      }).catch(err => {
+        this._credentialCheck.checking = false;
+        if (err && err.message === FAILED_TO_FETCH_ERROR) {
+          this.fire('auth-error');
+          this._cache.delete('/accounts/self/detail');
+        }
+      });
     },
 
     getDefaultPreferences() {
@@ -937,7 +1204,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     saveWatchedProjects(projects, opt_errFn) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'POST',
         url: '/accounts/self/watched.projects',
         body: projects,
@@ -952,7 +1219,7 @@
      * @param {function(?Response, string=)=} opt_errFn
      */
     deleteWatchedProjects(projects, opt_errFn) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'POST',
         url: '/accounts/self/watched.projects:delete',
         body: projects,
@@ -961,6 +1228,45 @@
       });
     },
 
+    /**
+     * @param {Defs.FetchJSONRequest} req
+     */
+    _fetchSharedCacheURL(req) {
+      if (this._sharedFetchPromises[req.url]) {
+        return this._sharedFetchPromises[req.url];
+      }
+      // TODO(andybons): Periodic cache invalidation.
+      if (this._cache.has(req.url)) {
+        return Promise.resolve(this._cache.get(req.url));
+      }
+      this._sharedFetchPromises[req.url] = this._fetchJSON(req)
+          .then(response => {
+            if (response !== undefined) {
+              this._cache.set(req.url, response);
+            }
+            this._sharedFetchPromises[req.url] = undefined;
+            return response;
+          }).catch(err => {
+            this._sharedFetchPromises[req.url] = undefined;
+            throw err;
+          });
+      return this._sharedFetchPromises[req.url];
+    },
+
+    /**
+     * @param {string} prefix
+     */
+    _invalidateSharedFetchPromisesPrefix(prefix) {
+      const newObject = {};
+      Object.entries(this._sharedFetchPromises).forEach(([key, value]) => {
+        if (!key.startsWith(prefix)) {
+          newObject[key] = value;
+        }
+      });
+      this._sharedFetchPromises = newObject;
+      this._cache.invalidatePrefix(prefix);
+    },
+
     _isNarrowScreen() {
       return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX;
     },
@@ -1002,7 +1308,7 @@
         params,
         reportUrlAsIs: true,
       };
-      return this._restApiHelper.fetchJSON(req).then(response => {
+      return this._fetchJSON(req).then(response => {
         // Response may be an array of changes OR an array of arrays of
         // changes.
         if (opt_query instanceof Array) {
@@ -1101,8 +1407,7 @@
      */
     _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) {
       return this.getChangeActionURL(changeNum, null, '/detail').then(url => {
-        const urlWithParams = this._restApiHelper
-            .urlWithParams(url, optionsHex);
+        const urlWithParams = this._urlWithParams(url, optionsHex);
         const params = {O: optionsHex};
         let req = {
           url,
@@ -1112,10 +1417,10 @@
           fetchOptions: this._etags.getOptions(urlWithParams),
           anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex,
         };
-        req = this._restApiHelper.addAcceptJsonHeader(req);
-        return this._restApiHelper.fetchRawJSON(req).then(response => {
+        req = this._addAcceptJsonHeader(req);
+        return this._fetchRawJSON(req).then(response => {
           if (response && response.status === 304) {
-            return Promise.resolve(this._restApiHelper.parsePrefixedJSON(
+            return Promise.resolve(this._parsePrefixedJSON(
                 this._etags.getCachedPayload(urlWithParams)));
           }
 
@@ -1129,7 +1434,7 @@
           }
 
           const payloadPromise = response ?
-              this._restApiHelper.readResponsePayload(response) :
+              this._readResponsePayload(response) :
               Promise.resolve(null);
 
           return payloadPromise.then(payload => {
@@ -1342,11 +1647,11 @@
     },
 
     invalidateGroupsCache() {
-      this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?');
+      this._invalidateSharedFetchPromisesPrefix('/groups/?');
     },
 
     invalidateReposCache() {
-      this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?');
+      this._invalidateSharedFetchPromisesPrefix('/projects/?');
     },
 
     /**
@@ -1384,7 +1689,7 @@
     setRepoHead(repo, ref) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/projects/${encodeURIComponent(repo)}/HEAD`,
         body: {ref},
@@ -1408,7 +1713,7 @@
       const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`;
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/branches?*',
@@ -1432,7 +1737,7 @@
           encodedFilter;
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/tags',
@@ -1451,7 +1756,7 @@
       const encodedFilter = this._computeFilter(filter);
       const n = pluginsPerPage + 1;
       const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`;
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url,
         errFn: opt_errFn,
         anonymizedUrl: '/plugins/?all',
@@ -1461,7 +1766,7 @@
     getRepoAccessRights(repoName, opt_errFn) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: `/projects/${encodeURIComponent(repoName)}/access`,
         errFn: opt_errFn,
         anonymizedUrl: '/projects/*/access',
@@ -1471,7 +1776,7 @@
     setRepoAccessRights(repoName, repoInfo) {
       // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend
       // supports it.
-      return this._restApiHelper.send({
+      return this._send({
         method: 'POST',
         url: `/projects/${encodeURIComponent(repoName)}/access`,
         body: repoInfo,
@@ -1480,7 +1785,7 @@
     },
 
     setRepoAccessRightsForReview(projectName, projectInfo) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: `/projects/${encodeURIComponent(projectName)}/access:review`,
         body: projectInfo,
@@ -1497,7 +1802,7 @@
     getSuggestedGroups(inputVal, opt_n, opt_errFn) {
       const params = {s: inputVal};
       if (opt_n) { params.n = opt_n; }
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/groups/',
         errFn: opt_errFn,
         params,
@@ -1517,7 +1822,7 @@
         type: 'ALL',
       };
       if (opt_n) { params.n = opt_n; }
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/projects/',
         errFn: opt_errFn,
         params,
@@ -1536,7 +1841,7 @@
       }
       const params = {suggest: null, q: inputVal};
       if (opt_n) { params.n = opt_n; }
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/accounts/',
         errFn: opt_errFn,
         params,
@@ -1567,7 +1872,7 @@
                 throw Error('Unsupported HTTP method: ' + method);
             }
 
-            return this._restApiHelper.send({method, url, body});
+            return this._send({method, url, body});
           });
     },
 
@@ -1597,7 +1902,7 @@
         O: options,
         q: 'status:open is:mergeable conflicts:' + changeNum,
       };
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/changes/',
         params,
         anonymizedUrl: '/changes/conflicts:*',
@@ -1619,7 +1924,7 @@
         O: options,
         q: query,
       };
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/changes/',
         params,
         anonymizedUrl: '/changes/change:*',
@@ -1642,7 +1947,7 @@
         O: options,
         q: query,
       };
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/changes/',
         params,
         anonymizedUrl: '/changes/topic:*',
@@ -1688,7 +1993,7 @@
         this.getChangeActionURL(changeNum, patchNum, '/review'),
       ];
       return Promise.all(promises).then(([, url]) => {
-        return this._restApiHelper.send({
+        return this._send({
           method: 'POST',
           url,
           body: review,
@@ -1722,7 +2027,7 @@
      */
     createChange(project, branch, subject, opt_topic, opt_isPrivate,
         opt_workInProgress, opt_baseChange, opt_baseCommit) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'POST',
         url: '/changes/',
         body: {
@@ -1897,7 +2202,7 @@
       return this.getFromProjectLookup(changeNum).then(project => {
         const url = '/accounts/self/starred.changes/' +
             (project ? encodeURIComponent(project) + '~' : '') + changeNum;
-        return this._restApiHelper.send({
+        return this._send({
           method: starred ? 'PUT' : 'DELETE',
           url,
           anonymizedUrl: '/accounts/self/starred.changes/*',
@@ -1914,7 +2219,59 @@
     },
 
     /**
-     * Public version of the _restApiHelper.send method preserved for plugins.
+     * Send an XHR.
+     * @param {Defs.SendRequest} req
+     * @return {Promise}
+     */
+    _send(req) {
+      const options = {method: req.method};
+      if (req.body) {
+        options.headers = new Headers();
+        options.headers.set(
+            'Content-Type', req.contentType || 'application/json');
+        options.body = typeof req.body === 'string' ?
+            req.body : JSON.stringify(req.body);
+      }
+      if (req.headers) {
+        if (!options.headers) { options.headers = new Headers(); }
+        for (const header in req.headers) {
+          if (!req.headers.hasOwnProperty(header)) { continue; }
+          options.headers.set(header, req.headers[header]);
+        }
+      }
+      const url = req.url.startsWith('http') ?
+          req.url : this.getBaseUrl() + req.url;
+      const fetchReq = {
+        url,
+        fetchOptions: options,
+        anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
+      };
+      const xhr = this._fetch(fetchReq).then(response => {
+        if (!response.ok) {
+          if (req.errFn) {
+            return req.errFn.call(undefined, response);
+          }
+          this.fire('server-error', {request: fetchReq, response});
+        }
+        return response;
+      }).catch(err => {
+        this.fire('network-error', {error: err});
+        if (req.errFn) {
+          return req.errFn.call(undefined, null, err);
+        } else {
+          throw err;
+        }
+      });
+
+      if (req.parseResponse) {
+        return xhr.then(res => this.getResponseObject(res));
+      }
+
+      return xhr;
+    },
+
+    /**
+     * Public version of the _send method preserved for plugins.
      * @param {string} method
      * @param {string} url
      * @param {?string|number|Object=} opt_body passed as null sometimes
@@ -1927,7 +2284,7 @@
      */
     send(method, url, opt_body, opt_errFn, opt_contentType,
         opt_headers) {
-      return this._restApiHelper.send({
+      return this._send({
         method,
         url,
         body: opt_body,
@@ -2184,7 +2541,7 @@
     },
 
     getCommitInfo(project, commit) {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/projects/' + encodeURIComponent(project) +
             '/commits/' + encodeURIComponent(commit),
         anonymizedUrl: '/projects/*/comments/*',
@@ -2192,7 +2549,7 @@
     },
 
     _fetchB64File(url) {
-      return this._restApiHelper.fetch({url: this.getBaseUrl() + url})
+      return this._fetch({url: this.getBaseUrl() + url})
           .then(response => {
             if (!response.ok) {
               return Promise.reject(new Error(response.statusText));
@@ -2316,7 +2673,7 @@
     },
 
     deleteAccountHttpPassword() {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: '/accounts/self/password.http',
         reportUrlAsIs: true,
@@ -2329,7 +2686,7 @@
      * parameter.
      */
     generateAccountHttpPassword() {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'PUT',
         url: '/accounts/self/password.http',
         body: {generate: true},
@@ -2353,7 +2710,7 @@
         contentType: 'plain/text',
         reportUrlAsIs: true,
       };
-      return this._restApiHelper.send(req)
+      return this._send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
               return Promise.reject(new Error('error'));
@@ -2367,7 +2724,7 @@
     },
 
     deleteAccountSSHKey(id) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: '/accounts/self/sshkeys/' + id,
         anonymizedUrl: '/accounts/self/sshkeys/*',
@@ -2375,7 +2732,7 @@
     },
 
     getAccountGPGKeys() {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/accounts/self/gpgkeys',
         reportUrlAsIs: true,
       });
@@ -2388,7 +2745,7 @@
         body: key,
         reportUrlAsIs: true,
       };
-      return this._restApiHelper.send(req)
+      return this._send(req)
           .then(response => {
             if (response.status < 200 && response.status >= 300) {
               return Promise.reject(new Error('error'));
@@ -2402,7 +2759,7 @@
     },
 
     deleteAccountGPGKey(id) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'DELETE',
         url: '/accounts/self/gpgkeys/' + id,
         anonymizedUrl: '/accounts/self/gpgkeys/*',
@@ -2435,7 +2792,7 @@
         body: {token},
         reportUrlAsIs: true,
       };
-      return this._restApiHelper.send(req).then(response => {
+      return this._send(req).then(response => {
         if (response.status === 204) {
           return 'Email confirmed successfully.';
         }
@@ -2444,7 +2801,7 @@
     },
 
     getCapabilities(opt_errFn) {
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: '/config/server/capabilities',
         errFn: opt_errFn,
         reportUrlAsIs: true,
@@ -2550,7 +2907,7 @@
      */
     getChange(changeNum, opt_errFn) {
       // Cannot use _changeBaseURL, as this function is used by _projectLookup.
-      return this._restApiHelper.fetchJSON({
+      return this._fetchJSON({
         url: `/changes/?q=change:${changeNum}`,
         errFn: opt_errFn,
         anonymizedUrl: '/changes/?q=change:*',
@@ -2610,7 +2967,7 @@
           req.endpoint : req.anonymizedEndpoint;
 
       return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
-        return this._restApiHelper.send({
+        return this._send({
           method: req.method,
           url: url + req.endpoint,
           body: req.body,
@@ -2635,7 +2992,7 @@
       const anonymizedBaseUrl = req.patchNum ?
           ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL;
       return this._changeBaseURL(req.changeNum, req.patchNum).then(url => {
-        return this._restApiHelper.fetchJSON({
+        return this._fetchJSON({
           url: url + req.endpoint,
           errFn: req.errFn,
           params: req.params,
@@ -2762,7 +3119,7 @@
     },
 
     deleteDraftComments(query) {
-      return this._restApiHelper.send({
+      return this._send({
         method: 'POST',
         url: '/accounts/self/drafts:delete',
         body: {query},
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index ea71522..9d0d83a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -63,8 +63,116 @@
       sandbox.restore();
     });
 
+    suite('fetchJSON()', () => {
+      test('Sets header to accept application/json', () => {
+        const authFetchStub = sandbox.stub(element._auth, 'fetch')
+            .returns(Promise.resolve());
+        element._fetchJSON({url: '/dummy/url'});
+        assert.isTrue(authFetchStub.called);
+        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+            'application/json');
+      });
+
+      test('Use header option accept when provided', () => {
+        const authFetchStub = sandbox.stub(element._auth, 'fetch')
+            .returns(Promise.resolve());
+        const headers = new Headers();
+        headers.append('Accept', '*/*');
+        const fetchOptions = {headers};
+        element._fetchJSON({url: '/dummy/url', fetchOptions});
+        assert.isTrue(authFetchStub.called);
+        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
+            '*/*');
+      });
+    });
+
+    test('JSON prefix is properly removed', done => {
+      element._fetchJSON({url: '/dummy/url'}).then(obj => {
+        assert.deepEqual(obj, {hello: 'bonjour'});
+        done();
+      });
+    });
+
+    test('cached results', done => {
+      let n = 0;
+      sandbox.stub(element, '_fetchJSON', () => {
+        return Promise.resolve(++n);
+      });
+      const promises = [];
+      promises.push(element._fetchSharedCacheURL('/foo'));
+      promises.push(element._fetchSharedCacheURL('/foo'));
+      promises.push(element._fetchSharedCacheURL('/foo'));
+
+      Promise.all(promises).then(results => {
+        assert.deepEqual(results, [1, 1, 1]);
+        element._fetchSharedCacheURL('/foo').then(foo => {
+          assert.equal(foo, 1);
+          done();
+        });
+      });
+    });
+
+    test('cached promise', done => {
+      const promise = Promise.reject(new Error('foo'));
+      element._cache.set('/foo', promise);
+      element._fetchSharedCacheURL({url: '/foo'}).catch(p => {
+        assert.equal(p.message, 'foo');
+        done();
+      });
+    });
+
+    test('cache invalidation', () => {
+      element._cache.set('/foo/bar', 1);
+      element._cache.set('/bar', 2);
+      element._sharedFetchPromises['/foo/bar'] = 3;
+      element._sharedFetchPromises['/bar'] = 4;
+      element._invalidateSharedFetchPromisesPrefix('/foo/');
+      assert.isFalse(element._cache.has('/foo/bar'));
+      assert.isTrue(element._cache.has('/bar'));
+      assert.isUndefined(element._sharedFetchPromises['/foo/bar']);
+      assert.strictEqual(4, element._sharedFetchPromises['/bar']);
+    });
+
+    test('params are properly encoded', () => {
+      let url = element._urlWithParams('/path/', {
+        sp: 'hola',
+        gr: 'guten tag',
+        noval: null,
+      });
+      assert.equal(url,
+          window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
+
+      url = element._urlWithParams('/path/', {
+        sp: 'hola',
+        en: ['hey', 'hi'],
+      });
+      assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
+
+      // Order must be maintained with array params.
+      url = element._urlWithParams('/path/', {
+        l: ['c', 'b', 'a'],
+      });
+      assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
+    });
+
+    test('request callbacks can be canceled', done => {
+      let cancelCalled = false;
+      window.fetch.returns(Promise.resolve({
+        body: {
+          cancel() { cancelCalled = true; },
+        },
+      }));
+      const cancelCondition = () => { return true; };
+      element._fetchJSON({url: '/dummy/url', cancelCondition}).then(
+          obj => {
+            assert.isUndefined(obj);
+            assert.isTrue(cancelCalled);
+            done();
+          });
+    });
+
     test('parent diff comments are properly grouped', done => {
-      sandbox.stub(element._restApiHelper, 'fetchJSON', () => {
+      sandbox.stub(element, '_fetchJSON', () => {
         return Promise.resolve({
           '/COMMIT_MSG': [],
           'sieve.go': [
@@ -207,7 +315,7 @@
     test('differing patch diff comments are properly grouped', done => {
       sandbox.stub(element, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
-      sandbox.stub(element._restApiHelper, 'fetchJSON', request => {
+      sandbox.stub(element, '_fetchJSON', request => {
         const url = request.url;
         if (url === '/changes/test~42/revisions/1') {
           return Promise.resolve({
@@ -324,7 +432,7 @@
     suite('rebase action', () => {
       let resolve_fetchJSON;
       setup(() => {
-        sandbox.stub(element._restApiHelper, 'fetchJSON').returns(
+        sandbox.stub(element, '_fetchJSON').returns(
             new Promise(resolve => {
               resolve_fetchJSON = resolve;
             }));
@@ -359,7 +467,7 @@
         element.addEventListener('server-error', resolve);
       });
 
-      element._restApiHelper.fetchJSON({}).then(response => {
+      element._fetchJSON({}).then(response => {
         assert.isUndefined(response);
         assert.isTrue(getResponseObjectStub.notCalled);
         serverErrorEventPromise.then(() => done());
@@ -375,12 +483,12 @@
           Promise.reject(new Error('Failed to fetch')));
       window.fetch.onSecondCall().returns(Promise.resolve(fakeAuthResponse));
       // Emulate logged in.
-      element._restApiHelper._cache.set('/accounts/self/detail', {});
+      element._cache.set('/accounts/self/detail', {});
       const serverErrorStub = sandbox.stub();
       element.addEventListener('server-error', serverErrorStub);
       const authErrorStub = sandbox.stub();
       element.addEventListener('auth-error', authErrorStub);
-      element._restApiHelper.fetchJSON({url: '/bar'}).finally(r => {
+      element._fetchJSON({url: '/bar'}).finally(r => {
         flush(() => {
           assert.isTrue(authErrorStub.called);
           assert.isFalse(serverErrorStub.called);
@@ -399,7 +507,7 @@
       element.addEventListener('server-error', serverErrorStub);
       const authErrorStub = sandbox.stub();
       element.addEventListener('auth-error', authErrorStub);
-      element._restApiHelper.fetchJSON({url: '/bar'}).finally(r => {
+      element._fetchJSON({url: '/bar'}).finally(r => {
         flush(() => {
           assert.isTrue(authErrorStub.called);
           assert.isFalse(serverErrorStub.called);
@@ -450,8 +558,7 @@
     test('checkCredentials promise rejection', () => {
       window.fetch.restore();
       element._cache.set('/accounts/self/detail', true);
-      const checkCredentialsSpy =
-          sandbox.spy(element._restApiHelper, 'checkCredentials');
+      sandbox.spy(element, 'checkCredentials');
       sandbox.stub(window, 'fetch', url => {
         return Promise.reject(new Error('Failed to fetch'));
       });
@@ -463,7 +570,7 @@
             // The second fetch call also fails, which leads to a second
             // invocation of checkCredentials, which should immediately
             // return instead of making further fetch calls.
-            assert.isTrue(checkCredentialsSpy .calledTwice);
+            assert.isTrue(element.checkCredentials.calledTwice);
             assert.isTrue(window.fetch.calledTwice);
           });
     });
@@ -478,7 +585,7 @@
     });
 
     test('legacy n,z key in change url is replaced', () => {
-      const stub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+      const stub = sandbox.stub(element, '_fetchJSON')
           .returns(Promise.resolve([]));
       element.getChanges(1, null, 'n,z');
       assert.equal(stub.lastCall.args[0].params.S, 0);
@@ -486,38 +593,38 @@
 
     test('saveDiffPreferences invalidates cache line', () => {
       const cacheKey = '/accounts/self/preferences.diff';
-      const sendStub = sandbox.stub(element._restApiHelper, 'send');
+      sandbox.stub(element, '_send');
       element._cache.set(cacheKey, {tab_size: 4});
       element.saveDiffPreferences({tab_size: 8});
-      assert.isTrue(sendStub.called);
-      assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+      assert.isTrue(element._send.called);
+      assert.isFalse(element._cache.has(cacheKey));
     });
 
     test('getAccount when resp is null does not add anything to the cache',
         done => {
           const cacheKey = '/accounts/self/detail';
-          const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+          const stub = sandbox.stub(element, '_fetchSharedCacheURL',
               () => Promise.resolve());
 
           element.getAccount().then(() => {
-            assert.isTrue(stub.called);
-            assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+            assert.isTrue(element._fetchSharedCacheURL.called);
+            assert.isFalse(element._cache.has(cacheKey));
             done();
           });
 
-          element._restApiHelper._cache.set(cacheKey, 'fake cache');
+          element._cache.set(cacheKey, 'fake cache');
           stub.lastCall.args[0].errFn();
         });
 
     test('getAccount does not add to the cache when resp.status is 403',
         done => {
           const cacheKey = '/accounts/self/detail';
-          const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+          const stub = sandbox.stub(element, '_fetchSharedCacheURL',
               () => Promise.resolve());
 
           element.getAccount().then(() => {
-            assert.isTrue(stub.called);
-            assert.isFalse(element._restApiHelper._cache.has(cacheKey));
+            assert.isTrue(element._fetchSharedCacheURL.called);
+            assert.isFalse(element._cache.has(cacheKey));
             done();
           });
           element._cache.set(cacheKey, 'fake cache');
@@ -526,15 +633,15 @@
 
     test('getAccount when resp is successful', done => {
       const cacheKey = '/accounts/self/detail';
-      const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL',
+      const stub = sandbox.stub(element, '_fetchSharedCacheURL',
           () => Promise.resolve());
 
       element.getAccount().then(response => {
-        assert.isTrue(stub.called);
-        assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache');
+        assert.isTrue(element._fetchSharedCacheURL.called);
+        assert.equal(element._cache.get(cacheKey), 'fake cache');
         done();
       });
-      element._restApiHelper._cache.set(cacheKey, 'fake cache');
+      element._cache.set(cacheKey, 'fake cache');
 
       stub.lastCall.args[0].errFn({});
     });
@@ -546,7 +653,7 @@
       sandbox.stub(element, '_isNarrowScreen', () => {
         return smallScreen;
       });
-      sandbox.stub(element._restApiHelper, 'fetchCacheURL', () => {
+      sandbox.stub(element, '_fetchSharedCacheURL', () => {
         return Promise.resolve(testJSON);
       });
     };
@@ -611,10 +718,10 @@
         });
 
     test('savPreferences normalizes download scheme', () => {
-      const sendStub = sandbox.stub(element._restApiHelper, 'send');
+      sandbox.stub(element, '_send');
       element.savePreferences({download_scheme: 'HTTP'});
-      assert.isTrue(sendStub.called);
-      assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http');
+      assert.isTrue(element._send.called);
+      assert.equal(element._send.lastCall.args[0].body.download_scheme, 'http');
     });
 
     test('getDiffPreferences returns correct defaults', done => {
@@ -640,10 +747,10 @@
     });
 
     test('saveDiffPreferences set show_tabs to false', () => {
-      const sendStub = sandbox.stub(element._restApiHelper, 'send');
+      sandbox.stub(element, '_send');
       element.saveDiffPreferences({show_tabs: false});
-      assert.isTrue(sendStub.called);
-      assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
+      assert.isTrue(element._send.called);
+      assert.equal(element._send.lastCall.args[0].body.show_tabs, false);
     });
 
     test('getEditPreferences returns correct defaults', done => {
@@ -673,36 +780,34 @@
     });
 
     test('saveEditPreferences set show_tabs to false', () => {
-      const sendStub = sandbox.stub(element._restApiHelper, 'send');
+      sandbox.stub(element, '_send');
       element.saveEditPreferences({show_tabs: false});
-      assert.isTrue(sendStub.called);
-      assert.equal(sendStub.lastCall.args[0].body.show_tabs, false);
+      assert.isTrue(element._send.called);
+      assert.equal(element._send.lastCall.args[0].body.show_tabs, false);
     });
 
     test('confirmEmail', () => {
-      const sendStub = sandbox.spy(element._restApiHelper, 'send');
+      sandbox.spy(element, '_send');
       element.confirmEmail('foo');
-      assert.isTrue(sendStub.calledOnce);
-      assert.equal(sendStub.lastCall.args[0].method, 'PUT');
-      assert.equal(sendStub.lastCall.args[0].url,
+      assert.isTrue(element._send.calledOnce);
+      assert.equal(element._send.lastCall.args[0].method, 'PUT');
+      assert.equal(element._send.lastCall.args[0].url,
           '/config/server/email.confirm');
-      assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'});
+      assert.deepEqual(element._send.lastCall.args[0].body, {token: 'foo'});
     });
 
     test('setAccountStatus', () => {
-      const sendStub = sandbox.stub(element._restApiHelper, 'send')
-        .returns(Promise.resolve('OOO'));
+      sandbox.stub(element, '_send').returns(Promise.resolve('OOO'));
       element._cache.set('/accounts/self/detail', {});
       return element.setAccountStatus('OOO').then(() => {
-        assert.isTrue(sendStub.calledOnce);
-        assert.equal(sendStub.lastCall.args[0].method, 'PUT');
-        assert.equal(sendStub.lastCall.args[0].url,
+        assert.isTrue(element._send.calledOnce);
+        assert.equal(element._send.lastCall.args[0].method, 'PUT');
+        assert.equal(element._send.lastCall.args[0].url,
             '/accounts/self/status');
-        assert.deepEqual(sendStub.lastCall.args[0].body,
+        assert.deepEqual(element._send.lastCall.args[0].body,
             {status: 'OOO'});
-        assert.deepEqual(element._restApiHelper
-            ._cache.get('/accounts/self/detail'),
-           {status: 'OOO'});
+        assert.deepEqual(element._cache.get('/accounts/self/detail'),
+            {status: 'OOO'});
       });
     });
 
@@ -791,20 +896,18 @@
       const change_num = '1';
       const file_name = 'index.php';
       const file_contents = '<?php';
-      sandbox.stub(element._restApiHelper, 'send').returns(
+      sandbox.stub(element, '_send').returns(
           Promise.resolve([change_num, file_name, file_contents]));
       sandbox.stub(element, 'getResponseObject')
           .returns(Promise.resolve([change_num, file_name, file_contents]));
       element._cache.set('/changes/' + change_num + '/edit/' + file_name, {});
       return element.saveChangeEdit(change_num, file_name, file_contents)
           .then(() => {
-            assert.isTrue(element._restApiHelper.send.calledOnce);
-            assert.equal(element._restApiHelper.send.lastCall.args[0].method,
-                'PUT');
-            assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+            assert.isTrue(element._send.calledOnce);
+            assert.equal(element._send.lastCall.args[0].method, 'PUT');
+            assert.equal(element._send.lastCall.args[0].url,
                 '/changes/test~1/edit/' + file_name);
-            assert.equal(element._restApiHelper.send.lastCall.args[0].body,
-                file_contents);
+            assert.equal(element._send.lastCall.args[0].body, file_contents);
           });
     });
 
@@ -812,18 +915,17 @@
       element._projectLookup = {1: 'test'};
       const change_num = '1';
       const message = 'this is a commit message';
-      sandbox.stub(element._restApiHelper, 'send').returns(
+      sandbox.stub(element, '_send').returns(
           Promise.resolve([change_num, message]));
       sandbox.stub(element, 'getResponseObject')
           .returns(Promise.resolve([change_num, message]));
       element._cache.set('/changes/' + change_num + '/message', {});
       return element.putChangeCommitMessage(change_num, message).then(() => {
-        assert.isTrue(element._restApiHelper.send.calledOnce);
-        assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT');
-        assert.equal(element._restApiHelper.send.lastCall.args[0].url,
+        assert.isTrue(element._send.calledOnce);
+        assert.equal(element._send.lastCall.args[0].method, 'PUT');
+        assert.equal(element._send.lastCall.args[0].url,
             '/changes/test~1/message');
-        assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body,
-            {message});
+        assert.deepEqual(element._send.lastCall.args[0].body, {message});
       });
     });
 
@@ -879,7 +981,7 @@
     });
 
     test('createRepo encodes name', () => {
-      const sendStub = sandbox.stub(element._restApiHelper, 'send')
+      const sendStub = sandbox.stub(element, '_send')
           .returns(Promise.resolve());
       return element.createRepo({name: 'x/y'}).then(() => {
         assert.isTrue(sendStub.calledOnce);
@@ -925,65 +1027,64 @@
 
     suite('getRepos', () => {
       const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only';
-      let fetchCacheURLStub;
+
       setup(() => {
-        fetchCacheURLStub =
-            sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+        sandbox.stub(element, '_fetchSharedCacheURL');
       });
 
       test('normal use', () => {
         element.getRepos('test', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=test');
 
         element.getRepos(null, 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             `/projects/?n=26&S=0&query=${defaultQuery}`);
 
         element.getRepos('test', 25, 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=25&query=test');
       });
 
       test('with blank', () => {
         element.getRepos('test/test', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest');
       });
 
       test('with hyphen', () => {
         element.getRepos('foo-bar', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('with leading hyphen', () => {
         element.getRepos('-bar', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Abar');
       });
 
       test('with trailing hyphen', () => {
         element.getRepos('foo-bar-', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('with underscore', () => {
         element.getRepos('foo_bar', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('with underscore', () => {
         element.getRepos('foo_bar', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar');
       });
 
       test('hyphen only', () => {
         element.getRepos('-', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             `/projects/?n=26&S=0&query=${defaultQuery}`);
       });
     });
@@ -1012,45 +1113,43 @@
     });
 
     suite('getGroups', () => {
-      let fetchCacheURLStub;
       setup(() => {
-        fetchCacheURLStub =
-            sandbox.stub(element._restApiHelper, 'fetchCacheURL');
+        sandbox.stub(element, '_fetchSharedCacheURL');
       });
 
       test('normal use', () => {
         element.getGroups('test', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/groups/?n=26&S=0&m=test');
 
         element.getGroups(null, 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/groups/?n=26&S=0');
 
         element.getGroups('test', 25, 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/groups/?n=26&S=25&m=test');
       });
 
       test('regex', () => {
         element.getGroups('^test.*', 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/groups/?n=26&S=0&r=%5Etest.*');
 
         element.getGroups('^test.*', 25, 25);
-        assert.equal(fetchCacheURLStub.lastCall.args[0].url,
+        assert.equal(element._fetchSharedCacheURL.lastCall.args[0].url,
             '/groups/?n=26&S=25&r=%5Etest.*');
       });
     });
 
     test('gerrit auth is used', () => {
       sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve());
-      element._restApiHelper.fetchJSON({url: 'foo'});
+      element._fetchJSON({url: 'foo'});
       assert(Gerrit.Auth.fetch.called);
     });
 
     test('getSuggestedAccounts does not return _fetchJSON', () => {
-      const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON');
+      const _fetchJSONSpy = sandbox.spy(element, '_fetchJSON');
       return element.getSuggestedAccounts().then(accts => {
         assert.isFalse(_fetchJSONSpy.called);
         assert.equal(accts.length, 0);
@@ -1058,7 +1157,7 @@
     });
 
     test('_fetchJSON gets called by getSuggestedAccounts', () => {
-      const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON',
+      const _fetchJSONStub = sandbox.stub(element, '_fetchJSON',
           () => Promise.resolve());
       return element.getSuggestedAccounts('own').then(() => {
         assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, {
@@ -1130,7 +1229,7 @@
         const errFn = sinon.stub();
         sandbox.stub(element, 'getChangeActionURL')
             .returns(Promise.resolve(''));
-        sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+        sandbox.stub(element, '_fetchRawJSON')
             .returns(Promise.resolve({ok: false, status: 500}));
         return element._getChangeDetail(123, '516714', errFn).then(() => {
           assert.isTrue(errFn.called);
@@ -1150,15 +1249,14 @@
       test('_getChangeDetail populates _projectLookup', () => {
         sandbox.stub(element, 'getChangeActionURL')
             .returns(Promise.resolve(''));
-        sandbox.stub(element._restApiHelper, 'fetchRawJSON')
+        sandbox.stub(element, '_fetchRawJSON')
             .returns(Promise.resolve({ok: true}));
 
         const mockResponse = {_number: 1, project: 'test'};
-        sandbox.stub(element._restApiHelper, 'readResponsePayload')
-            .returns(Promise.resolve({
-              parsed: mockResponse,
-              raw: JSON.stringify(mockResponse),
-            }));
+        sandbox.stub(element, '_readResponsePayload').returns(Promise.resolve({
+          parsed: mockResponse,
+          raw: JSON.stringify(mockResponse),
+        }));
         return element._getChangeDetail(1, '516714').then(() => {
           assert.equal(Object.keys(element._projectLookup).length, 1);
           assert.equal(element._projectLookup[1], 'test');
@@ -1176,8 +1274,7 @@
           const mockResponse = {foo: 'bar', baz: 42};
           mockResponseSerial = element.JSON_PREFIX +
               JSON.stringify(mockResponse);
-          sandbox.stub(element._restApiHelper, 'urlWithParams')
-              .returns(requestUrl);
+          sandbox.stub(element, '_urlWithParams').returns(requestUrl);
           sandbox.stub(element, 'getChangeActionURL')
               .returns(Promise.resolve(requestUrl));
           collectSpy = sandbox.spy(element._etags, 'collect');
@@ -1185,12 +1282,11 @@
         });
 
         test('contributes to cache', () => {
-          sandbox.stub(element._restApiHelper, 'fetchRawJSON')
-              .returns(Promise.resolve({
-                text: () => Promise.resolve(mockResponseSerial),
-                status: 200,
-                ok: true,
-              }));
+          sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({
+            text: () => Promise.resolve(mockResponseSerial),
+            status: 200,
+            ok: true,
+          }));
 
           return element._getChangeDetail(123, '516714').then(detail => {
             assert.isFalse(getPayloadSpy.called);
@@ -1201,12 +1297,11 @@
         });
 
         test('uses cache on HTTP 304', () => {
-          sandbox.stub(element._restApiHelper, 'fetchRawJSON')
-              .returns(Promise.resolve({
-                text: () => Promise.resolve(mockResponseSerial),
-                status: 304,
-                ok: true,
-              }));
+          sandbox.stub(element, '_fetchRawJSON').returns(Promise.resolve({
+            text: () => Promise.resolve(mockResponseSerial),
+            status: 304,
+            ok: true,
+          }));
 
           return element._getChangeDetail(123, {}).then(detail => {
             assert.isFalse(collectSpy.called);
@@ -1251,7 +1346,7 @@
 
     suite('getChanges populates _projectLookup', () => {
       test('multiple queries', () => {
-        sandbox.stub(element._restApiHelper, 'fetchJSON')
+        sandbox.stub(element, '_fetchJSON')
             .returns(Promise.resolve([
               [
                 {_number: 1, project: 'test'},
@@ -1271,7 +1366,7 @@
       });
 
       test('no query', () => {
-        sandbox.stub(element._restApiHelper, 'fetchJSON')
+        sandbox.stub(element, '_fetchJSON')
             .returns(Promise.resolve([
               {_number: 1, project: 'test'},
               {_number: 2, project: 'test'},
@@ -1291,7 +1386,7 @@
 
     test('_getChangeURLAndFetch', () => {
       element._projectLookup = {1: 'test'};
-      const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON')
+      const fetchStub = sandbox.stub(element, '_fetchJSON')
           .returns(Promise.resolve());
       const req = {changeNum: 1, endpoint: '/test', patchNum: 1};
       return element._getChangeURLAndFetch(req).then(() => {
@@ -1302,7 +1397,7 @@
 
     test('_getChangeURLAndSend', () => {
       element._projectLookup = {1: 'test'};
-      const sendStub = sandbox.stub(element._restApiHelper, 'send')
+      const sendStub = sandbox.stub(element, '_send')
           .returns(Promise.resolve());
 
       const req = {
@@ -1324,17 +1419,16 @@
         const mockObject = {foo: 'bar', baz: 'foo'};
         const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
         const mockResponse = {text: () => Promise.resolve(serial)};
-        return element._restApiHelper.readResponsePayload(mockResponse)
-            .then(payload => {
-              assert.deepEqual(payload.parsed, mockObject);
-              assert.equal(payload.raw, serial);
-            });
+        return element._readResponsePayload(mockResponse).then(payload => {
+          assert.deepEqual(payload.parsed, mockObject);
+          assert.equal(payload.raw, serial);
+        });
       });
 
       test('_parsePrefixedJSON', () => {
         const obj = {x: 3, y: {z: 4}, w: 23};
         const serial = element.JSON_PREFIX + JSON.stringify(obj);
-        const result = element._restApiHelper.parsePrefixedJSON(serial);
+        const result = element._parsePrefixedJSON(serial);
         assert.deepEqual(result, obj);
       });
     });
@@ -1356,7 +1450,7 @@
     });
 
     test('generateAccountHttpPassword', () => {
-      const sendSpy = sandbox.spy(element._restApiHelper, 'send');
+      const sendSpy = sandbox.spy(element, '_send');
       return element.generateAccountHttpPassword().then(() => {
         assert.isTrue(sendSpy.calledOnce);
         assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true});
@@ -1441,12 +1535,11 @@
     });
 
     test('getDashboard', () => {
-      const fetchCacheURLStub = sandbox.stub(element._restApiHelper,
-          'fetchCacheURL');
+      const fetchStub = sandbox.stub(element, '_fetchSharedCacheURL');
       element.getDashboard('gerrit/project', 'default:main');
-      assert.isTrue(fetchCacheURLStub.calledOnce);
+      assert.isTrue(fetchStub.calledOnce);
       assert.equal(
-          fetchCacheURLStub.lastCall.args[0].url,
+          fetchStub.lastCall.args[0].url,
           '/projects/gerrit%2Fproject/dashboards/default%3Amain');
     });
 
@@ -1514,7 +1607,7 @@
     });
 
     test('_fetch forwards request and logs', () => {
-      const logStub = sandbox.stub(element._restApiHelper, '_logCall');
+      const logStub = sandbox.stub(element, '_logCall');
       const response = {status: 404, text: sinon.stub()};
       const url = 'my url';
       const fetchOptions = {method: 'DELETE'};
@@ -1522,7 +1615,7 @@
       const startTime = 123;
       sandbox.stub(Date, 'now').returns(startTime);
       const req = {url, fetchOptions};
-      return element._restApiHelper.fetch(req).then(() => {
+      return element._fetch(req).then(() => {
         assert.isTrue(logStub.calledOnce);
         assert.isTrue(logStub.calledWith(req, startTime, response.status));
         assert.isFalse(response.text.called);
@@ -1534,11 +1627,10 @@
       const handler = sinon.stub();
       element.addEventListener('rpc-log', handler);
 
-      element._restApiHelper._logCall({url: 'url'}, 100, 200);
+      element._logCall({url: 'url'}, 100, 200);
       assert.isFalse(handler.called);
 
-      element._restApiHelper
-          ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
+      element._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200);
       flushAsynchronousOperations();
       assert.isTrue(handler.calledOnce);
     });
@@ -1547,7 +1639,7 @@
       sandbox.stub(element, 'getFromProjectLookup')
           .returns(Promise.resolve('test'));
       const sendStub =
-          sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve());
+          sandbox.stub(element, '_send').returns(Promise.resolve());
 
       await element.saveChangeStarred(123, true);
       assert.isTrue(sendStub.calledOnce);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
deleted file mode 100644
index d42abc3..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.js
+++ /dev/null
@@ -1,456 +0,0 @@
-/**
- * @license
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-(function(window) {
-  'use strict';
-
-  const Defs = {};
-
-  /**
-   * @typedef {{
-   *    url: string,
-   *    fetchOptions: (Object|null|undefined),
-   *    anonymizedUrl: (string|undefined),
-   * }}
-   */
-  Defs.FetchRequest;
-
-  /**
-   * Object to describe a request for passing into fetchJSON or fetchRawJSON.
-   * - url is the URL for the request (excluding get params)
-   * - errFn is a function to invoke when the request fails.
-   * - cancelCondition is a function that, if provided and returns true, will
-   *     cancel the response after it resolves.
-   * - params is a key-value hash to specify get params for the request URL.
-   * @typedef {{
-   *    url: string,
-   *    errFn: (function(?Response, string=)|null|undefined),
-   *    cancelCondition: (function()|null|undefined),
-   *    params: (Object|null|undefined),
-   *    fetchOptions: (Object|null|undefined),
-   *    anonymizedUrl: (string|undefined),
-   *    reportUrlAsIs: (boolean|undefined),
-   * }}
-   */
-  Defs.FetchJSONRequest;
-
-  const JSON_PREFIX = ')]}\'';
-  const FAILED_TO_FETCH_ERROR = 'Failed to fetch';
-
-  /**
-   * Wrapper around Map for caching server responses. Site-based so that
-   * changes to CANONICAL_PATH will result in a different cache going into
-   * effect.
-   */
-  class SiteBasedCache {
-    constructor() {
-      // Container of per-canonical-path caches.
-      this._data = new Map();
-      if (window.INITIAL_DATA != undefined) {
-        // Put all data shipped with index.html into the cache. This makes it
-        // so that we spare more round trips to the server when the app loads
-        // initially.
-        Object
-            .entries(window.INITIAL_DATA)
-            .forEach(e => this._cache().set(e[0], e[1]));
-      }
-    }
-
-    // Returns the cache for the current canonical path.
-    _cache() {
-      if (!this._data.has(window.CANONICAL_PATH)) {
-        this._data.set(window.CANONICAL_PATH, new Map());
-      }
-      return this._data.get(window.CANONICAL_PATH);
-    }
-
-    has(key) {
-      return this._cache().has(key);
-    }
-
-    get(key) {
-      return this._cache().get(key);
-    }
-
-    set(key, value) {
-      this._cache().set(key, value);
-    }
-
-    delete(key) {
-      this._cache().delete(key);
-    }
-
-    invalidatePrefix(prefix) {
-      const newMap = new Map();
-      for (const [key, value] of this._cache().entries()) {
-        if (!key.startsWith(prefix)) {
-          newMap.set(key, value);
-        }
-      }
-      this._data.set(window.CANONICAL_PATH, newMap);
-    }
-  }
-
-  class FetchPromisesCache {
-    constructor() {
-      this._data = {};
-    }
-
-    has(key) {
-      return !!this._data[key];
-    }
-
-    get(key) {
-      return this._data[key];
-    }
-
-    set(key, value) {
-      this._data[key] = value;
-    }
-
-    invalidatePrefix(prefix) {
-      const newData = {};
-      Object.entries(this._data).forEach(([key, value]) => {
-        if (!key.startsWith(prefix)) {
-          newData[key] = value;
-        }
-      });
-      this._data = newData;
-    }
-  }
-
-  class GrRestApiHelper {
-    /**
-     * @param {SiteBasedCache} cache
-     * @param {object} auth
-     * @param {FetchPromisesCache} fetchPromisesCache
-     * @param {object} credentialCheck
-     * @param {object} restApiInterface
-     */
-    constructor(cache, auth, fetchPromisesCache, credentialCheck,
-        restApiInterface) {
-      this._cache = cache;// TODO: make it public
-      this._auth = auth;
-      this._fetchPromisesCache = fetchPromisesCache;
-      this._credentialCheck = credentialCheck;
-      this._restApiInterface = restApiInterface;
-    }
-
-    /**
-     * Wraps calls to the underlying authenticated fetch function (_auth.fetch)
-     * with timing and logging.
-     * @param {Defs.FetchRequest} req
-     */
-    fetch(req) {
-      const start = Date.now();
-      const xhr = this._auth.fetch(req.url, req.fetchOptions);
-
-      // Log the call after it completes.
-      xhr.then(res => this._logCall(req, start, res ? res.status : null));
-
-      // Return the XHR directly (without the log).
-      return xhr;
-    }
-
-    /**
-     * Log information about a REST call. Because the elapsed time is determined
-     * by this method, it should be called immediately after the request
-     * finishes.
-     * @param {Defs.FetchRequest} req
-     * @param {number} startTime the time that the request was started.
-     * @param {number} status the HTTP status of the response. The status value
-     *     is used here rather than the response object so there is no way this
-     *     method can read the body stream.
-     */
-    _logCall(req, startTime, status) {
-      const method = (req.fetchOptions && req.fetchOptions.method) ?
-          req.fetchOptions.method : 'GET';
-      const endTime = Date.now();
-      const elapsed = (endTime - startTime);
-      const startAt = new Date(startTime);
-      const endAt = new Date(endTime);
-      console.log([
-        'HTTP',
-        status,
-        method,
-        elapsed + 'ms',
-        req.anonymizedUrl || req.url,
-        `(${startAt.toISOString()}, ${endAt.toISOString()})`,
-      ].join(' '));
-      if (req.anonymizedUrl) {
-        this.fire('rpc-log',
-            {status, method, elapsed, anonymizedUrl: req.anonymizedUrl});
-      }
-    }
-
-    /**
-     * Fetch JSON from url provided.
-     * Returns a Promise that resolves to a native Response.
-     * Doesn't do error checking. Supports cancel condition. Performs auth.
-     * Validates auth expiry errors.
-     * @param {Defs.FetchJSONRequest} req
-     */
-    fetchRawJSON(req) {
-      const urlWithParams = this.urlWithParams(req.url, req.params);
-      const fetchReq = {
-        url: urlWithParams,
-        fetchOptions: req.fetchOptions,
-        anonymizedUrl: req.reportUrlAsIs ? urlWithParams : req.anonymizedUrl,
-      };
-      return this.fetch(fetchReq).then(res => {
-        if (req.cancelCondition && req.cancelCondition()) {
-          res.body.cancel();
-          return;
-        }
-        return res;
-      }).catch(err => {
-        const isLoggedIn = !!this._cache.get('/accounts/self/detail');
-        if (isLoggedIn && err && err.message === FAILED_TO_FETCH_ERROR) {
-          this.checkCredentials();
-        } else {
-          if (req.errFn) {
-            req.errFn.call(undefined, null, err);
-          } else {
-            this.fire('network-error', {error: err});
-          }
-        }
-        throw err;
-      });
-    }
-
-    /**
-     * Fetch JSON from url provided.
-     * Returns a Promise that resolves to a parsed response.
-     * Same as {@link fetchRawJSON}, plus error handling.
-     * @param {Defs.FetchJSONRequest} req
-     */
-    fetchJSON(req) {
-      req = this.addAcceptJsonHeader(req);
-      return this.fetchRawJSON(req).then(response => {
-        if (!response) {
-          return;
-        }
-        if (!response.ok) {
-          if (req.errFn) {
-            req.errFn.call(null, response);
-            return;
-          }
-          this.fire('server-error', {request: req, response});
-          return;
-        }
-        return response && this.getResponseObject(response);
-      });
-    }
-
-    /**
-     * @param {string} url
-     * @param {?Object|string=} opt_params URL params, key-value hash.
-     * @return {string}
-     */
-    urlWithParams(url, opt_params) {
-      if (!opt_params) { return this.getBaseUrl() + url; }
-
-      const params = [];
-      for (const p in opt_params) {
-        if (!opt_params.hasOwnProperty(p)) { continue; }
-        if (opt_params[p] == null) {
-          params.push(encodeURIComponent(p));
-          continue;
-        }
-        for (const value of [].concat(opt_params[p])) {
-          params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`);
-        }
-      }
-      return this.getBaseUrl() + url + '?' + params.join('&');
-    }
-
-    /**
-     * @param {!Object} response
-     * @return {?}
-     */
-    getResponseObject(response) {
-      return this.readResponsePayload(response)
-          .then(payload => payload.parsed);
-    }
-
-    /**
-     * @param {!Object} response
-     * @return {!Object}
-     */
-    readResponsePayload(response) {
-      return response.text().then(text => {
-        let result;
-        try {
-          result = this.parsePrefixedJSON(text);
-        } catch (_) {
-          result = null;
-        }
-        return {parsed: result, raw: text};
-      });
-    }
-
-    /**
-     * @param {string} source
-     * @return {?}
-     */
-    parsePrefixedJSON(source) {
-      return JSON.parse(source.substring(JSON_PREFIX.length));
-    }
-
-    /**
-     * @param {Defs.FetchJSONRequest} req
-     * @return {Defs.FetchJSONRequest}
-     */
-    addAcceptJsonHeader(req) {
-      if (!req.fetchOptions) req.fetchOptions = {};
-      if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers();
-      if (!req.fetchOptions.headers.has('Accept')) {
-        req.fetchOptions.headers.append('Accept', 'application/json');
-      }
-      return req;
-    }
-
-    getBaseUrl() {
-      return this._restApiInterface.getBaseUrl();
-    }
-
-    fire(type, detail, options) {
-      return this._restApiInterface.fire(type, detail, options);
-    }
-
-    /**
-     * @param {Defs.FetchJSONRequest} req
-     */
-    fetchCacheURL(req) {
-      if (this._fetchPromisesCache.has(req.url)) {
-        return this._fetchPromisesCache.get(req.url);
-      }
-      // TODO(andybons): Periodic cache invalidation.
-      if (this._cache.has(req.url)) {
-        return Promise.resolve(this._cache.get(req.url));
-      }
-      this._fetchPromisesCache.set(req.url,
-          this.fetchJSON(req).then(response => {
-            if (response !== undefined) {
-              this._cache.set(req.url, response);
-            }
-            this._fetchPromisesCache.set(req.url, undefined);
-            return response;
-          }).catch(err => {
-            this._fetchPromisesCache.set(req.url, undefined);
-            throw err;
-          })
-      );
-      return this._fetchPromisesCache.get(req.url);
-    }
-
-    /**
-     * Send an XHR.
-     * @param {Defs.SendRequest} req
-     * @return {Promise}
-     */
-    send(req) {
-      const options = {method: req.method};
-      if (req.body) {
-        options.headers = new Headers();
-        options.headers.set(
-            'Content-Type', req.contentType || 'application/json');
-        options.body = typeof req.body === 'string' ?
-            req.body : JSON.stringify(req.body);
-      }
-      if (req.headers) {
-        if (!options.headers) { options.headers = new Headers(); }
-        for (const header in req.headers) {
-          if (!req.headers.hasOwnProperty(header)) { continue; }
-          options.headers.set(header, req.headers[header]);
-        }
-      }
-      const url = req.url.startsWith('http') ?
-          req.url : this.getBaseUrl() + req.url;
-      const fetchReq = {
-        url,
-        fetchOptions: options,
-        anonymizedUrl: req.reportUrlAsIs ? url : req.anonymizedUrl,
-      };
-      const xhr = this.fetch(fetchReq).then(response => {
-        if (!response.ok) {
-          if (req.errFn) {
-            return req.errFn.call(undefined, response);
-          }
-          this.fire('server-error', {request: fetchReq, response});
-        }
-        return response;
-      }).catch(err => {
-        this.fire('network-error', {error: err});
-        if (req.errFn) {
-          return req.errFn.call(undefined, null, err);
-        } else {
-          throw err;
-        }
-      });
-
-      if (req.parseResponse) {
-        return xhr.then(res => this.getResponseObject(res));
-      }
-
-      return xhr;
-    }
-
-    checkCredentials() {
-      if (this._credentialCheck.checking) {
-        return;
-      }
-      this._credentialCheck.checking = true;
-      let req = {url: '/accounts/self/detail', reportUrlAsIs: true};
-      req = this.addAcceptJsonHeader(req);
-      // Skip the REST response cache.
-      return this.fetchRawJSON(req).then(res => {
-        if (!res) { return; }
-        if (res.status === 403) {
-          this.fire('auth-error');
-          this._cache.delete('/accounts/self/detail');
-        } else if (res.ok) {
-          return this.getResponseObject(res);
-        }
-      }).then(res => {
-        this._credentialCheck.checking = false;
-        if (res) {
-          this._cache.set('/accounts/self/detail', res);
-        }
-        return res;
-      }).catch(err => {
-        this._credentialCheck.checking = false;
-        if (err && err.message === FAILED_TO_FETCH_ERROR) {
-          this.fire('auth-error');
-          this._cache.delete('/accounts/self/detail');
-        }
-      });
-    }
-
-    /**
-     * @param {string} prefix
-     */
-    invalidateFetchPromisesPrefix(prefix) {
-      this._fetchPromisesCache.invalidatePrefix(prefix);
-      this._cache.invalidatePrefix(prefix);
-    }
-  }
-
-  window.SiteBasedCache = SiteBasedCache;
-  window.FetchPromisesCache = FetchPromisesCache;
-  window.GrRestApiHelper = GrRestApiHelper;
-})(window);
-
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
deleted file mode 100644
index 4eaf1bc..0000000
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
+++ /dev/null
@@ -1,177 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>gr-rest-api-helper</title>
-<script src="/test/common-test-setup.js"></script>
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<link rel="import" href="../../../../test/common-test-setup.html"/>
-<script src="../../../../scripts/util.js"></script>
-<script src="../gr-auth.js"></script>
-<script src="gr-rest-api-helper.js"></script>
-
-<script>void(0);</script>
-
-<script>
-  suite('gr-rest-api-helper tests', () => {
-    let helper;
-    let sandbox;
-    let cache;
-    let fetchPromisesCache;
-
-    setup(() => {
-      sandbox = sinon.sandbox.create();
-      cache = new SiteBasedCache();
-      fetchPromisesCache = new FetchPromisesCache();
-      const credentialCheck = {checking: false};
-
-      window.CANONICAL_PATH = 'testhelper';
-
-      const mockRestApiInterface = {
-        getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH),
-        fire: sinon.stub(),
-      };
-
-      const testJSON = ')]}\'\n{"hello": "bonjour"}';
-      sandbox.stub(window, 'fetch').returns(Promise.resolve({
-        ok: true,
-        text() {
-          return Promise.resolve(testJSON);
-        },
-      }));
-
-      helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache,
-          credentialCheck, mockRestApiInterface);
-    });
-
-    teardown(() => {
-      sandbox.restore();
-    });
-
-    suite('fetchJSON()', () => {
-      test('Sets header to accept application/json', () => {
-        const authFetchStub = sandbox.stub(helper._auth, 'fetch')
-            .returns(Promise.resolve());
-        helper.fetchJSON({url: '/dummy/url'});
-        assert.isTrue(authFetchStub.called);
-        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
-            'application/json');
-      });
-
-      test('Use header option accept when provided', () => {
-        const authFetchStub = sandbox.stub(helper._auth, 'fetch')
-            .returns(Promise.resolve());
-        const headers = new Headers();
-        headers.append('Accept', '*/*');
-        const fetchOptions = {headers};
-        helper.fetchJSON({url: '/dummy/url', fetchOptions});
-        assert.isTrue(authFetchStub.called);
-        assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'),
-            '*/*');
-      });
-    });
-
-    test('JSON prefix is properly removed', done => {
-      helper.fetchJSON({url: '/dummy/url'}).then(obj => {
-        assert.deepEqual(obj, {hello: 'bonjour'});
-        done();
-      });
-    });
-
-    test('cached results', done => {
-      let n = 0;
-      sandbox.stub(helper, 'fetchJSON', () => {
-        return Promise.resolve(++n);
-      });
-      const promises = [];
-      promises.push(helper.fetchCacheURL('/foo'));
-      promises.push(helper.fetchCacheURL('/foo'));
-      promises.push(helper.fetchCacheURL('/foo'));
-
-      Promise.all(promises).then(results => {
-        assert.deepEqual(results, [1, 1, 1]);
-        helper.fetchCacheURL('/foo').then(foo => {
-          assert.equal(foo, 1);
-          done();
-        });
-      });
-    });
-
-    test('cached promise', done => {
-      const promise = Promise.reject(new Error('foo'));
-      cache.set('/foo', promise);
-      helper.fetchCacheURL({url: '/foo'}).catch(p => {
-        assert.equal(p.message, 'foo');
-        done();
-      });
-    });
-
-    test('cache invalidation', () => {
-      cache.set('/foo/bar', 1);
-      cache.set('/bar', 2);
-      fetchPromisesCache.set('/foo/bar', 3);
-      fetchPromisesCache.set('/bar', 4);
-      helper.invalidateFetchPromisesPrefix('/foo/');
-      assert.isFalse(cache.has('/foo/bar'));
-      assert.isTrue(cache.has('/bar'));
-      assert.isUndefined(fetchPromisesCache.get('/foo/bar'));
-      assert.strictEqual(4, fetchPromisesCache.get('/bar'));
-    });
-
-    test('params are properly encoded', () => {
-      let url = helper.urlWithParams('/path/', {
-        sp: 'hola',
-        gr: 'guten tag',
-        noval: null,
-      });
-      assert.equal(url,
-          window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval');
-
-      url = helper.urlWithParams('/path/', {
-        sp: 'hola',
-        en: ['hey', 'hi'],
-      });
-      assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi');
-
-      // Order must be maintained with array params.
-      url = helper.urlWithParams('/path/', {
-        l: ['c', 'b', 'a'],
-      });
-      assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a');
-    });
-
-    test('request callbacks can be canceled', done => {
-      let cancelCalled = false;
-      window.fetch.returns(Promise.resolve({
-        body: {
-          cancel() { cancelCalled = true; },
-        },
-      }));
-      const cancelCondition = () => { return true; };
-      helper.fetchJSON({url: '/dummy/url', cancelCondition}).then(
-          obj => {
-            assert.isUndefined(obj);
-            assert.isTrue(cancelCalled);
-            done();
-          });
-    });
-  });
-</script>
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index ac0dbeb..3de6227 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -35,9 +35,6 @@
   'GrReviewerUpdatesParser',
   'GrCountStringFormatter',
   'GrThemeApi',
-  'SiteBasedCache',
-  'FetchPromisesCache',
-  'GrRestApiHelper',
   'moment',
   'page',
   'util',
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 9c06113..0fbc8f1 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -187,7 +187,6 @@
     'shared/gr-rest-api-interface/gr-auth_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
-    'shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html',
     'shared/gr-select/gr-select_test.html',
     'shared/gr-storage/gr-storage_test.html',
     'shared/gr-textarea/gr-textarea_test.html',