Bug 1388830 - upgrade kinto-http.js to 4.5.3. r?MattN draft
authorEthan Glasser-Camp <ethan@betacantrips.com>
Wed, 14 Feb 2018 12:48:09 -0500
changeset 754943 221b12e66d6d25c3d5230971742fe0d407cb7d2a
parent 754870 e293877d13a5236119adb706c97c55ea9e11868b
push id99067
push userbmo:eglassercamp@mozilla.com
push dateWed, 14 Feb 2018 17:52:31 +0000
reviewersMattN
bugs1388830
milestone60.0a1
Bug 1388830 - upgrade kinto-http.js to 4.5.3. r?MattN This introduces an `UnparseableResponseError`, which exposes the text of the actual non-JSON response. It's also catcheable by client code (i.e. ExtensionStorageSync.jsm) if we believe this error is common enough to be silenced. MozReview-Commit-ID: H3ADFBFJRKA
services/common/kinto-http-client.js
--- a/services/common/kinto-http-client.js
+++ b/services/common/kinto-http-client.js
@@ -18,20 +18,20 @@
  * This file is generated from kinto-http.js - do not modify directly.
  */
 
 const global = this;
 
 this.EXPORTED_SYMBOLS = ["KintoHttpClient"];
 
 /*
- * Version 4.3.4 - 1294207
+ * Version 4.5.3 - 5179c56
  */
 
-(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.KintoHttpClient = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.KintoHttpClient = f()}})(function(){var define,module,exports;return (function(){function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}return e})()({1:[function(require,module,exports){
 /*
  *
  * 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
  *
@@ -44,149 +44,161 @@ this.EXPORTED_SYMBOLS = ["KintoHttpClien
 
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _base = require("../src/base");
 
 var _base2 = _interopRequireDefault(_base);
 
+var _errors = require("../src/errors");
+
+var errors = _interopRequireWildcard(_errors);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
 function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
 
 ChromeUtils.import("resource://gre/modules/Timer.jsm");
-Cu.importGlobalProperties(['fetch']);
+Cu.importGlobalProperties(["fetch"]);
 const { EventEmitter } = ChromeUtils.import("resource://gre/modules/EventEmitter.jsm", {});
 
 let KintoHttpClient = class KintoHttpClient extends _base2.default {
   constructor(remote, options = {}) {
     const events = {};
     EventEmitter.decorate(events);
-    super(remote, _extends({ events }, options));
+    super(remote, { events, ...options });
   }
 };
+exports.default = KintoHttpClient;
+
+
+KintoHttpClient.errors = errors;
 
 // This fixes compatibility with CommonJS required by browserify.
 // See http://stackoverflow.com/questions/33505992/babel-6-changes-how-it-exports-default/33683495#33683495
-
-exports.default = KintoHttpClient;
 if (typeof module === "object") {
   module.exports = KintoHttpClient;
 }
 
-},{"../src/base":7}],2:[function(require,module,exports){
+},{"../src/base":7,"../src/errors":12}],2:[function(require,module,exports){
 var v1 = require('./v1');
 var v4 = require('./v4');
 
 var uuid = v4;
 uuid.v1 = v1;
 uuid.v4 = v4;
 
 module.exports = uuid;
 
 },{"./v1":5,"./v4":6}],3:[function(require,module,exports){
 /**
  * Convert array of 16 byte values to UUID string format of the form:
- * XXXXXXXX-XXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+ * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
  */
 var byteToHex = [];
 for (var i = 0; i < 256; ++i) {
   byteToHex[i] = (i + 0x100).toString(16).substr(1);
 }
 
 function bytesToUuid(buf, offset) {
   var i = offset || 0;
   var bth = byteToHex;
-  return  bth[buf[i++]] + bth[buf[i++]] +
+  return bth[buf[i++]] + bth[buf[i++]] +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] + '-' +
           bth[buf[i++]] + bth[buf[i++]] +
           bth[buf[i++]] + bth[buf[i++]] +
           bth[buf[i++]] + bth[buf[i++]];
 }
 
 module.exports = bytesToUuid;
 
 },{}],4:[function(require,module,exports){
 // Unique ID creation requires a high quality random # generator.  In the
 // browser this is a little complicated due to unknown quality of Math.random()
 // and inconsistent support for the `crypto` API.  We do the best we can via
 // feature-detection
-var rng;
 
-var crypto = global.crypto || global.msCrypto; // for IE 11
-if (crypto && crypto.getRandomValues) {
+// getRandomValues needs to be invoked in a context where "this" is a Crypto implementation.
+var getRandomValues = (typeof(crypto) != 'undefined' && crypto.getRandomValues.bind(crypto)) ||
+                      (typeof(msCrypto) != 'undefined' && msCrypto.getRandomValues.bind(msCrypto));
+if (getRandomValues) {
   // WHATWG crypto RNG - http://wiki.whatwg.org/wiki/Crypto
-  var rnds8 = new Uint8Array(16);
-  rng = function whatwgRNG() {
-    crypto.getRandomValues(rnds8);
+  var rnds8 = new Uint8Array(16); // eslint-disable-line no-undef
+
+  module.exports = function whatwgRNG() {
+    getRandomValues(rnds8);
     return rnds8;
   };
-}
-
-if (!rng) {
+} else {
   // Math.random()-based (RNG)
   //
   // If all else fails, use Math.random().  It's fast, but is of unspecified
   // quality.
-  var  rnds = new Array(16);
-  rng = function() {
+  var rnds = new Array(16);
+
+  module.exports = function mathRNG() {
     for (var i = 0, r; i < 16; i++) {
       if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
       rnds[i] = r >>> ((i & 0x03) << 3) & 0xff;
     }
 
     return rnds;
   };
 }
 
-module.exports = rng;
-
 },{}],5:[function(require,module,exports){
-// Unique ID creation requires a high quality random # generator.  We feature
-// detect to determine the best RNG source, normalizing to a function that
-// returns 128-bits of randomness, since that's what's usually required
 var rng = require('./lib/rng');
 var bytesToUuid = require('./lib/bytesToUuid');
 
 // **`v1()` - Generate time-based UUID**
 //
 // Inspired by https://github.com/LiosK/UUID.js
 // and http://docs.python.org/library/uuid.html
 
-// random #'s we need to init node and clockseq
-var _seedBytes = rng();
-
-// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
-var _nodeId = [
-  _seedBytes[0] | 0x01,
-  _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5]
-];
-
-// Per 4.2.2, randomize (14 bit) clockseq
-var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff;
+var _nodeId;
+var _clockseq;
 
 // Previous uuid creation time
-var _lastMSecs = 0, _lastNSecs = 0;
+var _lastMSecs = 0;
+var _lastNSecs = 0;
 
 // See https://github.com/broofa/node-uuid for API details
 function v1(options, buf, offset) {
   var i = buf && offset || 0;
   var b = buf || [];
 
   options = options || {};
+  var node = options.node || _nodeId;
+  var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
 
-  var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq;
+  // node and clockseq need to be initialized to random values if they're not
+  // specified.  We do this lazily to minimize issues related to insufficient
+  // system entropy.  See #189
+  if (node == null || clockseq == null) {
+    var seedBytes = rng();
+    if (node == null) {
+      // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1)
+      node = _nodeId = [
+        seedBytes[0] | 0x01,
+        seedBytes[1], seedBytes[2], seedBytes[3], seedBytes[4], seedBytes[5]
+      ];
+    }
+    if (clockseq == null) {
+      // Per 4.2.2, randomize (14 bit) clockseq
+      clockseq = _clockseq = (seedBytes[6] << 8 | seedBytes[7]) & 0x3fff;
+    }
+  }
 
   // UUID timestamps are 100 nano-second units since the Gregorian epoch,
   // (1582-10-15 00:00).  JSNumbers aren't precise enough for this, so
   // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs'
   // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00.
   var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime();
 
   // Per 4.2.1.2, use count of uuid's generated during the current clock
@@ -237,17 +249,16 @@ function v1(options, buf, offset) {
 
   // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant)
   b[i++] = clockseq >>> 8 | 0x80;
 
   // `clock_seq_low`
   b[i++] = clockseq & 0xff;
 
   // `node`
-  var node = options.node || _nodeId;
   for (var n = 0; n < 6; ++n) {
     b[i + n] = node[n];
   }
 
   return buf ? buf : bytesToUuid(b);
 }
 
 module.exports = v1;
@@ -255,17 +266,17 @@ module.exports = v1;
 },{"./lib/bytesToUuid":3,"./lib/rng":4}],6:[function(require,module,exports){
 var rng = require('./lib/rng');
 var bytesToUuid = require('./lib/bytesToUuid');
 
 function v4(options, buf, offset) {
   var i = buf && offset || 0;
 
   if (typeof(options) == 'string') {
-    buf = options == 'binary' ? new Array(16) : null;
+    buf = options === 'binary' ? new Array(16) : null;
     options = null;
   }
   options = options || {};
 
   var rnds = options.random || (options.rng || rng)();
 
   // Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
   rnds[6] = (rnds[6] & 0x0f) | 0x40;
@@ -286,18 +297,16 @@ module.exports = v4;
 },{"./lib/bytesToUuid":3,"./lib/rng":4}],7:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = exports.SUPPORTED_PROTOCOL_VERSION = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _dec, _dec2, _dec3, _dec4, _dec5, _dec6, _dec7, _desc, _value, _class;
 
 var _utils = require("./utils");
 
 var _http = require("./http");
 
 var _http2 = _interopRequireDefault(_http);
 
@@ -355,18 +364,18 @@ function _applyDecoratedDescriptor(targe
 const SUPPORTED_PROTOCOL_VERSION = exports.SUPPORTED_PROTOCOL_VERSION = "v1";
 
 /**
  * High level HTTP client for the Kinto API.
  *
  * @example
  * const client = new KintoClient("https://kinto.dev.mozaws.net/v1");
  * client.bucket("default")
-*    .collection("my-blog")
-*    .createRecord({title: "First article"})
+ *    .collection("my-blog")
+ *    .createRecord({title: "First article"})
  *   .then(console.log.bind(console))
  *   .catch(console.error.bind(console));
  */
 let KintoClientBase = (_dec = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec2 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec3 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec4 = (0, _utils.nobatch)("This operation is not supported within a batch operation."), _dec5 = (0, _utils.nobatch)("Can't use batch within a batch!"), _dec6 = (0, _utils.capable)(["permissions_endpoint"]), _dec7 = (0, _utils.support)("1.4", "2.0"), (_class = class KintoClientBase {
   /**
    * Constructor.
    *
    * @param  {String}       remote  The remote URL.
@@ -500,50 +509,66 @@ let KintoClientBase = (_dec = (0, _utils
       batch: this._isBatch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options),
       retry: this._getRetry(options)
     });
   }
 
   /**
+   * Set client "headers" for every request, updating previous headers (if any).
+   *
+   * @param {Object} headers The headers to merge with existing ones.
+   */
+  setHeaders(headers) {
+    this._headers = {
+      ...this._headers,
+      ...headers
+    };
+    this.serverInfo = null;
+  }
+
+  /**
    * Get the value of "headers" for a given request, merging the
    * per-request headers with our own "default" headers.
    *
    * Note that unlike other options, headers aren't overridden, but
    * merged instead.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Object}
    */
   _getHeaders(options) {
-    return _extends({}, this._headers, options.headers);
+    return {
+      ...this._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "safe" for a given request, using the
    * per-request option if present or falling back to our default
    * otherwise.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Boolean}
    */
   _getSafe(options) {
-    return _extends({ safe: this._safe }, options).safe;
+    return { safe: this._safe, ...options }.safe;
   }
 
   /**
    * As _getSafe, but for "retry".
    *
    * @private
    */
   _getRetry(options) {
-    return _extends({ retry: this._retry }, options).retry;
+    return { retry: this._retry, ...options }.retry;
   }
 
   /**
    * Retrieves the server's "hello" endpoint. This endpoint reveals
    * server capabilities and settings as well as telling the client
    * "who they are" according to their given authorization headers.
    *
    * @private
@@ -782,29 +807,31 @@ let KintoClientBase = (_dec = (0, _utils
    * @param  {Number}  [options.retry=0]
    *     Number of times to retry each request if the server responds
    *     with Retry-After.
    */
   async paginatedList(path, params, options = {}) {
     // FIXME: this is called even in batch requests, which doesn't
     // make any sense (since all batch requests get a "dummy"
     // response; see execute() above).
-    const { sort, filters, limit, pages, since } = _extends({
-      sort: "-last_modified"
-    }, params);
+    const { sort, filters, limit, pages, since } = {
+      sort: "-last_modified",
+      ...params
+    };
     // Safety/Consistency check on ETag value.
     if (since && typeof since !== "string") {
       throw new Error(`Invalid value for since (${since}), should be ETag value.`);
     }
 
-    const querystring = (0, _utils.qsify)(_extends({}, filters, {
+    const querystring = (0, _utils.qsify)({
+      ...filters,
       _sort: sort,
       _limit: limit,
       _since: since
-    }));
+    });
     let results = [],
         current = 0;
 
     const next = async function (nextPage) {
       if (!nextPage) {
         throw new Error("Pagination exhausted.");
       }
       return processNextPage(nextPage);
@@ -867,17 +894,17 @@ let KintoClientBase = (_dec = (0, _utils
    *     when faced with transient errors.
    * @return {Promise<Object[], Error>}
    */
 
   async listPermissions(options = {}) {
     const path = (0, _endpoint2.default)("permissions");
     // Ensure the default sort parameter is something that exists in permissions
     // entries, as `last_modified` doesn't; here, we pick "id".
-    const paginationOptions = _extends({ sort: "id" }, options);
+    const paginationOptions = { sort: "id", ...options };
     return this.paginatedList(path, paginationOptions, {
       headers: this._getHeaders(options),
       retry: this._getRetry(options)
     });
   }
 
   /**
    * Retrieves the list of buckets.
@@ -935,17 +962,17 @@ let KintoClientBase = (_dec = (0, _utils
    * @return {Promise<Object, Error>}
    */
   async deleteBucket(bucket, options = {}) {
     const bucketObj = (0, _utils.toDataBody)(bucket);
     if (!bucketObj.id) {
       throw new Error("A bucket id is required.");
     }
     const path = (0, _endpoint2.default)("bucket", bucketObj.id);
-    const { last_modified } = _extends({}, bucketObj, options);
+    const { last_modified } = { ...bucketObj, ...options };
     return this.execute(requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     }), { retry: this._getRetry(options) });
   }
 
   /**
@@ -997,17 +1024,17 @@ function aggregate(responses = [], reque
   };
   return responses.reduce((acc, response, index) => {
     const { status } = response;
     const request = requests[index];
     if (status >= 200 && status < 400) {
       acc.published.push(response.body);
     } else if (status === 404) {
       // Extract the id manually from request path while waiting for Kinto/kinto#818
-      const regex = /(buckets|groups|collections|records)\/([^\/]+)$/;
+      const regex = /(buckets|groups|collections|records)\/([^/]+)$/;
       const extracts = request.path.match(regex);
       const id = extracts.length === 3 ? extracts[2] : undefined;
       acc.skipped.push({
         id,
         path: request.path,
         error: response.body
       });
     } else if (status === 412) {
@@ -1031,18 +1058,16 @@ function aggregate(responses = [], reque
 },{}],9:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _dec, _desc, _value, _class;
 
 var _utils = require("./utils");
 
 var _collection = require("./collection");
 
 var _collection2 = _interopRequireDefault(_collection);
 
@@ -1126,39 +1151,42 @@ let Bucket = (_dec = (0, _utils.capable)
 
   /**
    * Get the value of "headers" for a given request, merging the
    * per-request headers with our own "default" headers.
    *
    * @private
    */
   _getHeaders(options) {
-    return _extends({}, this._headers, options.headers);
+    return {
+      ...this._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "safe" for a given request, using the
    * per-request option if present or falling back to our default
    * otherwise.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Boolean}
    */
   _getSafe(options) {
-    return _extends({ safe: this._safe }, options).safe;
+    return { safe: this._safe, ...options }.safe;
   }
 
   /**
    * As _getSafe, but for "retry".
    *
    * @private
    */
   _getRetry(options) {
-    return _extends({ retry: this._retry }, options).retry;
+    return { retry: this._retry, ...options }.retry;
   }
 
   /**
    * Selects a collection.
    *
    * @param  {String}  name              The collection name.
    * @param  {Object}  [options={}]      The options object.
    * @param  {Object}  [options.headers] The headers object option.
@@ -1206,28 +1234,28 @@ let Bucket = (_dec = (0, _utils.capable)
    * @param  {Number}  [options.last_modified] The last_modified option.
    * @return {Promise<Object, Error>}
    */
   async setData(data, options = {}) {
     if (!(0, _utils.isObject)(data)) {
       throw new Error("A bucket object is required.");
     }
 
-    const bucket = _extends({}, data, { id: this.name });
+    const bucket = { ...data, id: this.name };
 
     // For default bucket, we need to drop the id from the data object.
     // Bug in Kinto < 3.1.1
     const bucketId = bucket.id;
     if (bucket.id === "default") {
       delete bucket.id;
     }
 
     const path = (0, _endpoint2.default)("bucket", bucketId);
     const { patch, permissions } = options;
-    const { last_modified } = _extends({}, data, options);
+    const { last_modified } = { ...data, ...options };
     const request = requests.updateRequest(path, { data: bucket, permissions }, {
       last_modified,
       patch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1304,17 +1332,17 @@ let Bucket = (_dec = (0, _utils.capable)
    * @return {Promise<Object, Error>}
    */
   async deleteCollection(collection, options = {}) {
     const collectionObj = (0, _utils.toDataBody)(collection);
     if (!collectionObj.id) {
       throw new Error("A collection id is required.");
     }
     const { id } = collectionObj;
-    const { last_modified } = _extends({}, collectionObj, options);
+    const { last_modified } = { ...collectionObj, ...options };
     const path = (0, _endpoint2.default)("collection", this.name, id);
     const request = requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1364,20 +1392,21 @@ let Bucket = (_dec = (0, _utils.capable)
    * @param  {Object}            [options.permissions] The permissions object.
    * @param  {Boolean}           [options.safe]        The safe option.
    * @param  {Object}            [options.headers]     The headers object option.
    * @param  {Number}            [options.retry=0]     Number of retries to make
    *     when faced with transient errors.
    * @return {Promise<Object, Error>}
    */
   async createGroup(id, members = [], options = {}) {
-    const data = _extends({}, options.data, {
+    const data = {
+      ...options.data,
       id,
       members
-    });
+    };
     const path = (0, _endpoint2.default)("group", this.name, id);
     const { permissions } = options;
     const request = requests.createRequest(path, { data, permissions }, {
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1398,20 +1427,23 @@ let Bucket = (_dec = (0, _utils.capable)
    */
   async updateGroup(group, options = {}) {
     if (!(0, _utils.isObject)(group)) {
       throw new Error("A group object is required.");
     }
     if (!group.id) {
       throw new Error("A group id is required.");
     }
-    const data = _extends({}, options.data, group);
+    const data = {
+      ...options.data,
+      ...group
+    };
     const path = (0, _endpoint2.default)("group", this.name, group.id);
     const { patch, permissions } = options;
-    const { last_modified } = _extends({}, data, options);
+    const { last_modified } = { ...data, ...options };
     const request = requests.updateRequest(path, { data, permissions }, {
       last_modified,
       patch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1426,17 +1458,17 @@ let Bucket = (_dec = (0, _utils.capable)
    *     when faced with transient errors.
    * @param  {Boolean}       [options.safe]          The safe option.
    * @param  {Number}        [options.last_modified] The last_modified option.
    * @return {Promise<Object, Error>}
    */
   async deleteGroup(group, options = {}) {
     const groupObj = (0, _utils.toDataBody)(group);
     const { id } = groupObj;
-    const { last_modified } = _extends({}, groupObj, options);
+    const { last_modified } = { ...groupObj, ...options };
     const path = (0, _endpoint2.default)("group", this.name, id);
     const request = requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -1565,18 +1597,16 @@ exports.default = Bucket;
 },{"./collection":10,"./endpoint":11,"./requests":14,"./utils":15}],10:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _dec, _dec2, _dec3, _desc, _value, _class;
 
 var _uuid = require("uuid");
 
 var _utils = require("./utils");
 
 var _requests = require("./requests");
 
@@ -1659,49 +1689,55 @@ let Collection = (_dec = (0, _utils.capa
 
     /**
      * @ignore
      */
     this._retry = options.retry || 0;
     this._safe = !!options.safe;
     // FIXME: This is kind of ugly; shouldn't the bucket be responsible
     // for doing the merge?
-    this._headers = _extends({}, this.bucket._headers, options.headers);
+    this._headers = {
+      ...this.bucket._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "headers" for a given request, merging the
    * per-request headers with our own "default" headers.
    *
    * @private
    */
   _getHeaders(options) {
-    return _extends({}, this._headers, options.headers);
+    return {
+      ...this._headers,
+      ...options.headers
+    };
   }
 
   /**
    * Get the value of "safe" for a given request, using the
    * per-request option if present or falling back to our default
    * otherwise.
    *
    * @private
    * @param {Object} options The options for a request.
    * @returns {Boolean}
    */
   _getSafe(options) {
-    return _extends({ safe: this._safe }, options).safe;
+    return { safe: this._safe, ...options }.safe;
   }
 
   /**
    * As _getSafe, but for "retry".
    *
    * @private
    */
   _getRetry(options) {
-    return _extends({ retry: this._retry }, options).retry;
+    return { retry: this._retry, ...options }.retry;
   }
 
   /**
    * Retrieves the total number of records in this collection.
    *
    * @param  {Object} [options={}]      The options object.
    * @param  {Object} [options.headers] The headers object option.
    * @param  {Number} [options.retry=0] Number of retries to make
@@ -1752,17 +1788,17 @@ let Collection = (_dec = (0, _utils.capa
    * @param  {Number}   [options.last_modified] The last_modified option.
    * @return {Promise<Object, Error>}
    */
   async setData(data, options = {}) {
     if (!(0, _utils.isObject)(data)) {
       throw new Error("A collection object is required.");
     }
     const { patch, permissions } = options;
-    const { last_modified } = _extends({}, data, options);
+    const { last_modified } = { ...data, ...options };
 
     const path = (0, _endpoint2.default)("collection", this.bucket.name, this.name);
     const request = requests.updateRequest(path, { data, permissions }, {
       last_modified,
       patch,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
@@ -1902,17 +1938,17 @@ let Collection = (_dec = (0, _utils.capa
    * @param  {String}  [options.gzipped]       Force the attachment to be gzipped or not.
    * @return {Promise<Object, Error>}
    */
 
   async addAttachment(dataURI, record = {}, options = {}) {
     const { permissions } = options;
     const id = record.id || _uuid.v4.v4();
     const path = (0, _endpoint2.default)("attachment", this.bucket.name, this.name, id);
-    const { last_modified } = _extends({}, record, options);
+    const { last_modified } = { ...record, ...options };
     const addAttachmentRequest = requests.addAttachmentRequest(path, dataURI, { data: record, permissions }, {
       last_modified,
       filename: options.filename,
       gzipped: options.gzipped,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     await this.client.execute(addAttachmentRequest, {
@@ -1961,17 +1997,17 @@ let Collection = (_dec = (0, _utils.capa
   async updateRecord(record, options = {}) {
     if (!(0, _utils.isObject)(record)) {
       throw new Error("A record object is required.");
     }
     if (!record.id) {
       throw new Error("A record id is required.");
     }
     const { permissions } = options;
-    const { last_modified } = _extends({}, record, options);
+    const { last_modified } = { ...record, ...options };
     const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, record.id);
     const request = requests.updateRequest(path, { data: record, permissions }, {
       headers: this._getHeaders(options),
       safe: this._getSafe(options),
       last_modified,
       patch: !!options.patch
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
@@ -1990,17 +2026,17 @@ let Collection = (_dec = (0, _utils.capa
    * @return {Promise<Object, Error>}
    */
   async deleteRecord(record, options = {}) {
     const recordObj = (0, _utils.toDataBody)(record);
     if (!recordObj.id) {
       throw new Error("A record id is required.");
     }
     const { id } = recordObj;
-    const { last_modified } = _extends({}, recordObj, options);
+    const { last_modified } = { ...recordObj, ...options };
     const path = (0, _endpoint2.default)("record", this.bucket.name, this.name, id);
     const request = requests.deleteRequest(path, {
       last_modified,
       headers: this._getHeaders(options),
       safe: this._getSafe(options)
     });
     return this.client.execute(request, { retry: this._getRetry(options) });
   }
@@ -2095,17 +2131,18 @@ let Collection = (_dec = (0, _utils.capa
       throw new Error("Computing a snapshot is only possible when the full history for a " + "collection is available. Here, the history plugin seems to have " + "been enabled after the creation of the collection.");
     }
     const { data: changes } = await this.bucket.listHistory({
       pages: Infinity, // all pages up to target timestamp are required
       sort: "-target.data.last_modified",
       filters: {
         resource_name: "record",
         collection_id: this.name,
-        "max_target.data.last_modified": String(at) }
+        "max_target.data.last_modified": String(at) // eq. to <=
+      }
     });
     return changes;
   }
 
   /**
    * @private
    */
 
@@ -2113,19 +2150,17 @@ let Collection = (_dec = (0, _utils.capa
     if (!Number.isInteger(at) || at <= 0) {
       throw new Error("Invalid argument, expected a positive integer.");
     }
     // Retrieve history and check it covers the required time range.
     const changes = await this.listChangesBackTo(at);
     // Replay changes to compute the requested snapshot.
     const seenIds = new Set();
     let snapshot = [];
-    for (const _ref of changes) {
-      const { action, target: { data: record } } = _ref;
-
+    for (const { action, target: { data: record } } of changes) {
       if (action == "delete") {
         seenIds.add(record.id); // ensure not reprocessing deleted entries
         snapshot = snapshot.filter(r => r.id !== record.id);
       } else if (!seenIds.has(record.id)) {
         seenIds.add(record.id);
         snapshot.push(record);
       }
     }
@@ -2204,17 +2239,17 @@ function endpoint(name, ...args) {
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 /**
  * Kinto server error code descriptors.
  * @type {Object}
  */
-exports.default = {
+const ERROR_CODES = {
   104: "Missing Authorization Token",
   105: "Invalid Authorization Token",
   106: "Request body was not valid JSON",
   107: "Invalid request parameter",
   108: "Missing request parameter",
   109: "Invalid posted data",
   110: "Invalid Token / id",
   111: "Missing Token / id",
@@ -2226,34 +2261,112 @@ exports.default = {
   117: "Client has sent too many requests",
   121: "Resource access is forbidden for this user",
   122: "Another resource violates constraint",
   201: "Service Temporary unavailable due to high load",
   202: "Service deprecated",
   999: "Internal Server Error"
 };
 
+exports.default = ERROR_CODES;
+let NetworkTimeoutError = class NetworkTimeoutError extends Error {
+  constructor(url, options) {
+    super(`Timeout while trying to access ${url} with ${JSON.stringify(options)}`);
+
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, NetworkTimeoutError);
+    }
+
+    this.url = url;
+    this.options = options;
+  }
+};
+let UnparseableResponseError = class UnparseableResponseError extends Error {
+  constructor(response, body, error) {
+    const { status } = response;
+
+    super(`Response from server unparseable (HTTP ${status || 0}; ${error}): ${body}`);
+
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, UnparseableResponseError);
+    }
+
+    this.status = status;
+    this.response = response;
+    this.stack = error.stack;
+    this.error = error;
+  }
+};
+
+/**
+ * "Error" subclass representing a >=400 response from the server.
+ *
+ * Whether or not this is an error depends on your application.
+ *
+ * The `json` field can be undefined if the server responded with an
+ * empty response body. This shouldn't generally happen. Most "bad"
+ * responses come with a JSON error description, or (if they're
+ * fronted by a CDN or nginx or something) occasionally non-JSON
+ * responses (which become UnparseableResponseErrors, above).
+ */
+
+let ServerResponse = class ServerResponse extends Error {
+  constructor(response, json) {
+    const { status } = response;
+    let { statusText } = response;
+    let errnoMsg;
+
+    if (json) {
+      // Try to fill in information from the JSON error.
+      statusText = json.error || statusText;
+
+      // Take errnoMsg from either ERROR_CODES or json.message.
+      if (json.errno && json.errno in ERROR_CODES) {
+        errnoMsg = ERROR_CODES[json.errno];
+      } else if (json.message) {
+        errnoMsg = json.message;
+      }
+
+      // If we had both ERROR_CODES and json.message, and they differ,
+      // combine them.
+      if (errnoMsg && json.message && json.message !== errnoMsg) {
+        errnoMsg += ` (${json.message})`;
+      }
+    }
+
+    let message = `HTTP ${status} ${statusText}`;
+    if (errnoMsg) {
+      message += `: ${errnoMsg}`;
+    }
+
+    super(message.trim());
+    if (Error.captureStackTrace) {
+      Error.captureStackTrace(this, ServerResponse);
+    }
+
+    this.response = response;
+    this.data = json;
+  }
+};
+exports.NetworkTimeoutError = NetworkTimeoutError;
+exports.ServerResponse = ServerResponse;
+exports.UnparseableResponseError = UnparseableResponseError;
+
 },{}],13:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
 exports.default = undefined;
 
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 var _utils = require("./utils");
 
 var _errors = require("./errors");
 
-var _errors2 = _interopRequireDefault(_errors);
-
-function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
-
 /**
  * Enhanced HTTP client for the Kinto protocol.
  * @private
  */
 let HTTP = class HTTP {
   /**
    * Default HTTP request headers applied to each outgoing request.
    *
@@ -2314,17 +2427,17 @@ let HTTP = class HTTP {
   timedFetch(url, options) {
     let hasTimedout = false;
     return new Promise((resolve, reject) => {
       // Detect if a request has timed out.
       let _timeoutId;
       if (this.timeout) {
         _timeoutId = setTimeout(() => {
           hasTimedout = true;
-          reject(new Error("Request timeout."));
+          reject(new _errors.NetworkTimeoutError(url, options));
         }, this.timeout);
       }
       function proceedWithHandler(fn) {
         return arg => {
           if (!hasTimedout) {
             if (_timeoutId) {
               clearTimeout(_timeoutId);
             }
@@ -2335,62 +2448,39 @@ let HTTP = class HTTP {
       fetch(url, options).then(proceedWithHandler(resolve)).catch(proceedWithHandler(reject));
     });
   }
 
   /**
    * @private
    */
   async processResponse(response) {
-    const { status } = response;
+    const { status, headers } = response;
     const text = await response.text();
     // Check if we have a body; if so parse it as JSON.
-    if (text.length === 0) {
-      return this.formatResponse(response, null);
-    }
-    try {
-      return this.formatResponse(response, JSON.parse(text));
-    } catch (err) {
-      const error = new Error(`HTTP ${status || 0}; ${err}`);
-      error.response = response;
-      error.stack = err.stack;
-      throw error;
+    let json;
+    if (text.length !== 0) {
+      try {
+        json = JSON.parse(text);
+      } catch (err) {
+        throw new _errors.UnparseableResponseError(response, text, err);
+      }
     }
-  }
-
-  /**
-   * @private
-   */
-  formatResponse(response, json) {
-    const { status, statusText, headers } = response;
-    if (json && status >= 400) {
-      let message = `HTTP ${status} ${json.error || ""}: `;
-      if (json.errno && json.errno in _errors2.default) {
-        const errnoMsg = _errors2.default[json.errno];
-        message += errnoMsg;
-        if (json.message && json.message !== errnoMsg) {
-          message += ` (${json.message})`;
-        }
-      } else {
-        message += statusText || "";
-      }
-      const error = new Error(message.trim());
-      error.response = response;
-      error.data = json;
-      throw error;
+    if (status >= 400) {
+      throw new _errors.ServerResponse(response, json);
     }
     return { status, json, headers };
   }
 
   /**
    * @private
    */
   async retry(url, retryAfter, request, options) {
     await (0, _utils.delay)(retryAfter);
-    return this.request(url, request, _extends({}, options, { retry: options.retry - 1 }));
+    return this.request(url, request, { ...options, retry: options.retry - 1 });
   }
 
   /**
    * Performs an HTTP request to the Kinto server.
    *
    * Resolves with an objet containing the following HTTP response properties:
    * - `{Number}  status`  The HTTP status code.
    * - `{Object}  json`    The JSON response body.
@@ -2402,17 +2492,17 @@ let HTTP = class HTTP {
    * @param  {Object} [request.headers] The request headers object (default: {})
    * @param  {Object} [options={}]      Options for making the
    *     request
    * @param  {Number} [options.retry]   Number of retries (default: 0)
    * @return {Promise}
    */
   async request(url, request = { headers: {} }, options = { retry: 0 }) {
     // Ensure default request headers are always set
-    request.headers = _extends({}, HTTP.DEFAULT_REQUEST_HEADERS, request.headers);
+    request.headers = { ...HTTP.DEFAULT_REQUEST_HEADERS, ...request.headers };
     // If a multipart body is provided, remove any custom Content-Type header as
     // the fetch() implementation will add the correct one for us.
     if (request.body && typeof request.body.append === "function") {
       delete request.headers["Content-Type"];
     }
     request.mode = this.requestMode;
 
     const response = await this.timedFetch(url, request);
@@ -2472,19 +2562,16 @@ let HTTP = class HTTP {
 exports.default = HTTP;
 
 },{"./errors":12,"./utils":15}],14:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 exports.createRequest = createRequest;
 exports.updateRequest = updateRequest;
 exports.jsonPatchPermissionsRequest = jsonPatchPermissionsRequest;
 exports.deleteRequest = deleteRequest;
 exports.addAttachmentRequest = addAttachmentRequest;
 
 var _utils = require("./utils");
 
@@ -2509,115 +2596,120 @@ function safeHeader(safe, last_modified)
   }
   return { "If-None-Match": "*" };
 }
 
 /**
  * @private
  */
 function createRequest(path, { data, permissions }, options = {}) {
-  const { headers, safe } = _extends({}, requestDefaults, options);
+  const { headers, safe } = {
+    ...requestDefaults,
+    ...options
+  };
   return {
     method: data && data.id ? "PUT" : "POST",
     path,
-    headers: _extends({}, headers, safeHeader(safe)),
+    headers: { ...headers, ...safeHeader(safe) },
     body: { data, permissions }
   };
 }
 
 /**
  * @private
  */
 function updateRequest(path, { data, permissions }, options = {}) {
-  const { headers, safe, patch } = _extends({}, requestDefaults, options);
-  const { last_modified } = _extends({}, data, options);
+  const { headers, safe, patch } = { ...requestDefaults, ...options };
+  const { last_modified } = { ...data, ...options };
 
   if (Object.keys((0, _utils.omit)(data, "id", "last_modified")).length === 0) {
     data = undefined;
   }
 
   return {
     method: patch ? "PATCH" : "PUT",
     path,
-    headers: _extends({}, headers, safeHeader(safe, last_modified)),
+    headers: { ...headers, ...safeHeader(safe, last_modified) },
     body: { data, permissions }
   };
 }
 
 /**
  * @private
  */
 function jsonPatchPermissionsRequest(path, permissions, opType, options = {}) {
-  const { headers, safe, last_modified } = _extends({}, requestDefaults, options);
+  const { headers, safe, last_modified } = { ...requestDefaults, ...options };
 
   const ops = [];
 
   for (const [type, principals] of Object.entries(permissions)) {
     for (const principal of principals) {
       ops.push({
         op: opType,
         path: `/permissions/${type}/${principal}`
       });
     }
   }
 
   return {
     method: "PATCH",
     path,
-    headers: _extends({}, headers, safeHeader(safe, last_modified), {
+    headers: {
+      ...headers,
+      ...safeHeader(safe, last_modified),
       "Content-Type": "application/json-patch+json"
-    }),
+    },
     body: ops
   };
 }
 
 /**
  * @private
  */
 function deleteRequest(path, options = {}) {
-  const { headers, safe, last_modified } = _extends({}, requestDefaults, options);
+  const { headers, safe, last_modified } = {
+    ...requestDefaults,
+    ...options
+  };
   if (safe && !last_modified) {
     throw new Error("Safe concurrency check requires a last_modified value.");
   }
   return {
     method: "DELETE",
     path,
-    headers: _extends({}, headers, safeHeader(safe, last_modified))
+    headers: { ...headers, ...safeHeader(safe, last_modified) }
   };
 }
 
 /**
  * @private
  */
 function addAttachmentRequest(path, dataURI, { data, permissions } = {}, options = {}) {
-  const { headers, safe, gzipped } = _extends({}, requestDefaults, options);
-  const { last_modified } = _extends({}, data, options);
+  const { headers, safe, gzipped } = { ...requestDefaults, ...options };
+  const { last_modified } = { ...data, ...options };
 
   const body = { data, permissions };
   const formData = (0, _utils.createFormData)(dataURI, body, options);
 
   let customPath = gzipped != null ? customPath = path + "?gzipped=" + (gzipped ? "true" : "false") : path;
 
   return {
     method: "POST",
     path: customPath,
-    headers: _extends({}, headers, safeHeader(safe, last_modified)),
+    headers: { ...headers, ...safeHeader(safe, last_modified) },
     body: formData
   };
 }
 
 },{"./utils":15}],15:[function(require,module,exports){
 "use strict";
 
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
-
-var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
-
 exports.partition = partition;
 exports.delay = delay;
 exports.pMap = pMap;
 exports.omit = omit;
 exports.toDataBody = toDataBody;
 exports.qsify = qsify;
 exports.checkVersion = checkVersion;
 exports.support = support;
@@ -2868,19 +2960,19 @@ function parseDataURL(dataURL) {
   if (!match) {
     throw new Error(`Invalid data-url: ${String(dataURL).substr(0, 32)}...`);
   }
   const props = match[1];
   const base64 = match[2];
   const [type, ...rawParams] = props.split(";");
   const params = rawParams.reduce((acc, param) => {
     const [key, value] = param.split("=");
-    return _extends({}, acc, { [key]: value });
+    return { ...acc, [key]: value };
   }, {});
-  return _extends({}, params, { type, base64 });
+  return { ...params, type, base64 };
 }
 
 /**
  * Extracts file information from a data url.
  * @param  {String} dataURL The data url.
  * @return {Object}
  */
 function extractFileInfo(dataURL) {