Bug 1153822: Adjust Marionette responses to match WebDriver protocol
authorAndreas Tolfsen <ato@mozilla.com>
Thu, 21 May 2015 11:26:58 +0100
changeset 281181 2a81ba282e1641eb0a1905cd789e5ce2af229bbb
parent 281180 9eaa5be9131856e1c1bc4c4af74ad531400910b0
child 281182 391d49ed7ca77855209c6c1ab7ce9bcb56ff1faa
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-esr52@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1153822
milestone43.0a1
Bug 1153822: Adjust Marionette responses to match WebDriver protocol Introduce protocol version levels in the Marionette server. On establishing a connection to a local end, the remote will return a `marionetteProtocol` field indicating which level it speaks. The protocol level can be used by local ends to either fall into compatibility mode or warn the user that the local end is incompatible with the remote. The protocol is currently also more expressive than it needs to be and this expressiveness has previously resulted in subtle inconsistencies in the fields returned. This patch reduces the amount of superfluous fields, reducing the amount of data sent. Aligning the protocol closer to the WebDriver specification's expectations will also reduce the amount of post-processing required in the httpd. Previous to this patch, this is a value response: {"from":"0","value":null,"status":0,"sessionId":"{6b6d68d2-4ac9-4308-9f07-d2e72519c407}"} And this for ok responses: {"from":"0","ok":true} And this for errors: {"from":"0","status":21,"sessionId":"{6b6d68d2-4ac9-4308-9f07-d2e72519c407}","error":{"message":"Error loading page, timed out (onDOMContentLoaded)","stacktrace":null,"status":21}} This patch drops the `from` and `sessionId` fields, and the `status` field from non-error responses. It also drops the `ok` field in non-value responses and flattens the error response to a simple dictionary with the `error` (previously `status`), `message`, and `stacktrace` properties, which are now all required. r=jgriffin
.hgignore
testing/marionette/actions.js
testing/marionette/command.js
testing/marionette/dispatcher.js
testing/marionette/driver.js
testing/marionette/error.js
testing/marionette/listener.js
--- a/.hgignore
+++ b/.hgignore
@@ -1,16 +1,17 @@
 # .hgignore - List of filenames hg should ignore
 
 # Filenames that should be ignored wherever they appear
 ~$
 \.py(c|o)$
 (?i)(^|/)TAGS$
 (^|/)ID$
 (^|/)\.DS_Store$
+.*\.egg-info
 
 # Vim swap files.
 ^\.sw.$
 .[^/]*\.sw.$
 
 # Emacs directory variable files.
 \.dir-locals\.el
 
--- a/testing/marionette/actions.js
+++ b/testing/marionette/actions.js
@@ -151,17 +151,17 @@ ActionChain.prototype.resetValues = func
  * Function to emit touch events for each finger. e.g.
  * finger=[['press', id], ['wait', 5], ['release']] touchId represents
  * the finger id, i keeps track of the current action of the chain
  * keyModifiers is an object keeping track keyDown/keyUp pairs through
  * an action chain.
  */
 ActionChain.prototype.actions = function(chain, touchId, i, keyModifiers) {
   if (i == chain.length) {
-    this.onSuccess({value: touchId});
+    this.onSuccess({value: touchId || null});
     this.resetValues();
     return;
   }
 
   let pack = chain[i];
   let command = pack[0];
   let el;
   let c;
--- a/testing/marionette/command.js
+++ b/testing/marionette/command.js
@@ -9,104 +9,99 @@ const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 Cu.import("chrome://marionette/content/error.js");
 
 this.EXPORTED_SYMBOLS = ["CommandProcessor", "Response"];
 const logger = Log.repository.getLogger("Marionette");
 
+const validator = {
+  exclusionary: {
+    "capabilities": ["error", "value"],
+    "error": ["value", "sessionId", "capabilities"],
+    "sessionId": ["error", "value"],
+    "value": ["error", "sessionId", "capabilities"],
+  },
+
+  set: function(obj, prop, val) {
+    let tests = this.exclusionary[prop];
+    if (tests) {
+      for (let t of tests) {
+        if (obj.hasOwnProperty(t)) {
+          throw new TypeError(`${t} set, cannot set ${prop}`);
+        }
+      }
+    }
+
+    obj[prop] = val;
+    return true;
+  },
+};
+
+/**
+ * The response body is exposed as an argument to commands.
+ * Commands can set fields on the body through defining properties.
+ *
+ * Setting properties invokes a validator that performs tests for
+ * mutually exclusionary fields on the input against the existing data
+ * in the body.
+ *
+ * For example setting the {@code error} property on the body when
+ * {@code value}, {@code sessionId}, or {@code capabilities} have been
+ * set previously will cause an error.
+ */
+this.ResponseBody = () => new Proxy({}, validator);
+
 /**
  * Represents the response returned from the remote end after execution
  * of its corresponding command.
  *
- * The Response is a mutable object passed to each command for
- * modification through the available setters.  The response is sent
- * implicitly by CommandProcessor when a command is finished executing,
- * and any modifications made subsequent to this will have no effect.
+ * The response is a mutable object passed to each command for
+ * modification through the available setters.  To send data in a response,
+ * you modify the body property on the response.  The body property can
+ * also be replaced completely.
+ *
+ * The response is sent implicitly by CommandProcessor when a command
+ * has finished executing, and any modifications made subsequent to that
+ * will have no effect.
  *
  * @param {number} cmdId
  *     UUID tied to the corresponding command request this is
  *     a response for.
- * @param {function(number)} okHandler
- *     Callback function called on successful responses with no body.
  * @param {function(Object, number)} respHandler
- *     Callback function called on successful responses with body.
- * @param {Object=} msg
- *     A message to populate the response, containing the properties
- *     "sessionId", "status", and "value".
- * @param {function(Map)=} sanitizer
- *     Run before sending message.
+ *     Callback function called on responses.
  */
-this.Response = function(cmdId, okHandler, respHandler, msg, sanitizer) {
-  const removeEmpty = function(map) {
-    let rv = {};
-    for (let [key, value] of map) {
-      if (typeof value == "undefined") {
-        value = null;
-      }
-      rv[key] = value;
-    }
-    return rv;
-  };
-
+this.Response = function(cmdId, respHandler) {
   this.id = cmdId;
-  this.ok = true;
-  this.okHandler = okHandler;
   this.respHandler = respHandler;
-  this.sanitizer = sanitizer || removeEmpty;
-
-  this.data = new Map([
-    ["sessionId", msg.sessionId ? msg.sessionId : null],
-    ["status", msg.status ? msg.status : "success"],
-    ["value", msg.value ? msg.value : undefined],
-  ]);
-};
-
-Response.prototype = {
-  get name() { return this.data.get("name"); },
-  set name(n) { this.data.set("name", n); },
-  get sessionId() { return this.data.get("sessionId"); },
-  set sessionId(id) { this.data.set("sessionId", id); },
-  get status() { return this.data.get("status"); },
-  set status(ns) { this.data.set("status", ns); },
-  get value() { return this.data.get("value"); },
-  set value(val) {
-    this.data.set("value", val);
-    this.ok = false;
-  }
+  this.sent = false;
+  this.body = ResponseBody();
 };
 
 Response.prototype.send = function() {
   if (this.sent) {
-    logger.warn("Skipped sending response to command ID " +
-      this.id + " because response has already been sent");
-    return;
+    throw new RangeError("Response has already been sent: " + this.toString());
   }
-
-  if (this.ok) {
-    this.okHandler(this.id);
-  } else {
-    let rawData = this.sanitizer(this.data);
-    this.respHandler(rawData, this.id);
-  }
+  this.respHandler(this.body, this.id);
+  this.sent = true;
 };
 
-/**
- * @param {(Error|Object)} err
- *     The error to send, either an instance of the Error prototype,
- *     or an object with the properties "message", "status", and "stack".
- */
 Response.prototype.sendError = function(err) {
-  this.status = "status" in err ? err.status : new UnknownError().status;
-  this.value = error.toJSON(err);
+  let wd = error.isWebDriverError(err);
+  let we = wd ? err : new WebDriverError(err.message);
+
+  this.body.error = we.status;
+  this.body.message = we.message || null;
+  this.body.stacktrace = we.stack || null;
+
   this.send();
 
   // propagate errors that are implementation problems
-  if (!error.isWebDriverError(err)) {
+  if (!wd) {
     throw err;
   }
 };
 
 /**
  * The command processor receives messages on execute(payload, …)
  * from the dispatcher, processes them, and wraps the functions that
  * it executes from the WebDriver implementation, driver.
@@ -126,38 +121,43 @@ this.CommandProcessor = function(driver)
  * The respHandler function will be called with the JSON object to
  * send back to the client.
  *
  * The cmdId is the UUID tied to this request that prevents
  * the dispatcher from sending responses in the wrong order.
  *
  * @param {Object} payload
  *     Message as received from client.
- * @param {function(number)} okHandler
- *     Callback function called on successful responses with no body.
  * @param {function(Object, number)} respHandler
- *     Callback function called on successful responses with body.
+ *     Callback function called on responses.
  * @param {number} cmdId
  *     The unique identifier for the command to execute.
  */
-CommandProcessor.prototype.execute = function(payload, okHandler, respHandler, cmdId) {
+CommandProcessor.prototype.execute = function(payload, respHandler, cmdId) {
   let cmd = payload;
-  let resp = new Response(
-    cmdId, okHandler, respHandler, {sessionId: this.driver.sessionId});
+  let resp = new Response(cmdId, respHandler);
   let sendResponse = resp.send.bind(resp);
   let sendError = resp.sendError.bind(resp);
 
   // Ideally handlers shouldn't have to care about the command ID,
   // but some methods (newSession, executeScript, et al.) have not
   // yet been converted to use the new form of request dispatching.
   cmd.id = cmdId;
 
   let req = Task.spawn(function*() {
     let fn = this.driver.commands[cmd.name];
     if (typeof fn == "undefined") {
       throw new UnknownCommandError(cmd.name);
     }
 
-    yield fn.bind(this.driver)(cmd, resp);
+    let rv = yield fn.bind(this.driver)(cmd, resp);
+
+    if (typeof rv != "undefined") {
+      if (typeof rv != "object") {
+        resp.body = {value: rv};
+      } else {
+        resp.body = rv;
+      }
+    }
   }.bind(this));
 
   req.then(sendResponse, sendError).catch(error.report);
 };
--- a/testing/marionette/dispatcher.js
+++ b/testing/marionette/dispatcher.js
@@ -12,16 +12,18 @@ Cu.import("resource://gre/modules/Task.j
 
 Cu.import("chrome://marionette/content/command.js");
 Cu.import("chrome://marionette/content/emulator.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/driver.js");
 
 this.EXPORTED_SYMBOLS = ["Dispatcher"];
 
+const PROTOCOL_VERSION = 2;
+
 const logger = Log.repository.getLogger("Marionette");
 const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 
 /**
  * Manages a Marionette connection, and dispatches packets received to
  * their correct destinations.
  *
  * @param {number} connId
@@ -33,22 +35,16 @@ const uuidGen = Cc["@mozilla.org/uuid-ge
  *     a GeckoDriver.
  * @param {function()} stopSignal
  *     Signal to stop the Marionette server.
  */
 this.Dispatcher = function(connId, transport, driverFactory, stopSignal) {
   this.id = connId;
   this.conn = transport;
 
-  // Marionette uses a protocol based on the debugger server, which
-  // requires passing back actor ID's with responses.  Unlike the debugger
-  // server, we don't actually have multiple actors, so just use a dummy
-  // value of "0".
-  this.actorId = "0";
-
   // callback for when connection is closed
   this.onclose = null;
 
   // transport hooks are Dispatcher.prototype.onPacket
   // and Dispatcher.prototype.onClosed
   this.conn.hooks = this;
 
   this.emulator = new Emulator(msg => this.sendResponse(msg, -1));
@@ -59,51 +55,42 @@ this.Dispatcher = function(connId, trans
 };
 
 /**
  * Debugger transport callback that dispatches the request.
  * Request handlers defined in this.requests take presedence
  * over those defined in this.driver.commands.
  */
 Dispatcher.prototype.onPacket = function(packet) {
-  // Avoid using toSource and template strings (or touching the payload at all
-  // if not necessary) for the sake of memory use.
-  // See https://bugzilla.mozilla.org/show_bug.cgi?id=1150170
   if (logger.level <= Log.Level.Debug) {
-    logger.debug(this.id + " -> (" + JSON.stringify(packet) + ")");
+    logger.debug(this.id + " -> " + JSON.stringify(packet));
   }
 
   if (this.requests && this.requests[packet.name]) {
     this.requests[packet.name].bind(this)(packet);
   } else {
     let id = this.beginNewCommand();
-    let ok = this.sendOk.bind(this);
     let send = this.send.bind(this);
-    this.commandProcessor.execute(packet, ok, send, id);
+    this.commandProcessor.execute(packet, send, id);
   }
 };
 
 /**
  * Debugger transport callback that cleans up
  * after a connection is closed.
  */
 Dispatcher.prototype.onClosed = function(status) {
   this.driver.sessionTearDown();
   if (this.onclose) {
     this.onclose(this);
   }
 };
 
 // Dispatcher specific command handlers:
 
-Dispatcher.prototype.getMarionetteID = function() {
-  let id = this.beginNewCommand();
-  this.sendResponse({from: "root", id: this.actorId}, id);
-};
-
 Dispatcher.prototype.emulatorCmdResult = function(msg) {
   switch (this.driver.context) {
     case Context.CONTENT:
       this.driver.sendAsync("emulatorCmdResult", msg);
       break;
     case Context.CHROME:
       let cb = this.emulator.popCallback(msg.id);
       if (!cb) {
@@ -117,20 +104,17 @@ Dispatcher.prototype.emulatorCmdResult =
 /**
  * Quits Firefox with the provided flags and tears down the current
  * session.
  */
 Dispatcher.prototype.quitApplication = function(msg) {
   let id = this.beginNewCommand();
 
   if (this.driver.appName != "Firefox") {
-    this.sendError({
-      "message": "In app initiated quit only supported on Firefox",
-      "status": "webdriver error",
-    }, id);
+    this.sendError(new WebDriverError("In app initiated quit only supported in Firefox"));
     return;
   }
 
   let flags = Ci.nsIAppStartup.eAttemptQuit;
   for (let k of msg.parameters.flags) {
     flags |= Ci.nsIAppStartup[k];
   }
 
@@ -140,100 +124,60 @@ Dispatcher.prototype.quitApplication = f
   this.driver.sessionTearDown();
   Services.startup.quit(flags);
 };
 
 // Convenience methods:
 
 Dispatcher.prototype.sayHello = function() {
   let id = this.beginNewCommand();
-  let yo = {from: "root", applicationType: "gecko", traits: []};
-  this.sendResponse(yo, id);
+  let whatHo = {
+    applicationType: "gecko",
+    marionetteProtocol: PROTOCOL_VERSION,
+  };
+  this.send(whatHo, id);
 };
 
 Dispatcher.prototype.sendOk = function(cmdId) {
-  this.sendResponse({from: this.actorId, ok: true}, cmdId);
+  this.send({}, cmdId);
 };
 
 Dispatcher.prototype.sendError = function(err, cmdId) {
-  let packet = {
-    from: this.actorId,
-    status: err.status,
-    sessionId: this.driver.sessionId,
-    error: err
-  };
-  this.sendResponse(packet, cmdId);
+  let resp = new Response(cmdId, this.send.bind(this));
+  resp.sendError(err);
 };
 
 /**
- * Marshals and sends message to either client or emulator based on the
- * provided {@code cmdId}.
- *
- * This routine produces a Marionette protocol packet, which is different
- * to a WebDriver protocol response in that it contains an extra key
- * {@code from} for the debugger transport actor ID.  It also replaces the
- * key {@code value} with {@code error} when {@code msg.status} isn't
- * {@code 0}.
- *
- * @param {Object} msg
- *     Object with the properties {@code value}, {@code status}, and
- *     {@code sessionId}.
- * @param {UUID} cmdId
- *     The unique identifier for the command the message is a response to.
- */
-Dispatcher.prototype.send = function(msg, cmdId) {
-  let packet = {
-    from: this.actorId,
-    value: msg.value,
-    status: msg.status,
-    sessionId: msg.sessionId,
-  };
-
-  if (typeof packet.value == "undefined") {
-    packet.value = null;
-  }
-
-  // the Marionette protocol sends errors using the "error"
-  // key instead of, as Selenium, "value"
-  if (!error.isSuccess(msg.status)) {
-    packet.error = packet.value;
-    delete packet.value;
-  }
-
-  this.sendResponse(packet, cmdId);
-};
-
-// Low-level methods:
-
-/**
  * Delegates message to client or emulator based on the provided
  * {@code cmdId}.  The message is sent over the debugger transport socket.
  *
  * The command ID is a unique identifier assigned to the client's request
  * that is used to distinguish the asynchronous responses.
  *
  * Whilst responses to commands are synchronous and must be sent in the
  * correct order, emulator callbacks are more transparent and can be sent
  * at any time.  These callbacks won't change the current command state.
  *
  * @param {Object} payload
  *     The payload to send.
  * @param {UUID} cmdId
  *     The unique identifier for this payload.  {@code -1} signifies
  *     that it's an emulator callback.
  */
-Dispatcher.prototype.sendResponse = function(payload, cmdId) {
+Dispatcher.prototype.send = function(payload, cmdId) {
   if (emulator.isCallback(cmdId)) {
     this.sendToEmulator(payload);
   } else {
     this.sendToClient(payload, cmdId);
     this.commandId = null;
   }
 };
 
+// Low-level methods:
+
 /**
  * Send message to emulator over the debugger transport socket.
  * Notably this skips out-of-sync command checks.
  */
 Dispatcher.prototype.sendToEmulator = function(payload) {
   this.sendRaw("emulator", payload);
 };
 
@@ -260,21 +204,18 @@ Dispatcher.prototype.sendToClient = func
   this.sendRaw("client", payload);
 };
 
 /**
  * Sends payload as-is over debugger transport socket to client,
  * and logs it.
  */
 Dispatcher.prototype.sendRaw = function(dest, payload) {
-  // Avoid using toSource and template strings (or touching the payload at all
-  // if not necessary) for the sake of memory use.
-  // See https://bugzilla.mozilla.org/show_bug.cgi?id=1150170
   if (logger.level <= Log.Level.Debug) {
-    logger.debug(this.id + " " + dest + " <- (" + JSON.stringify(payload) + ")");
+    logger.debug(this.id + " " + dest + " <- " + JSON.stringify(payload));
   }
   this.conn.send(payload);
 };
 
 /**
  * Begins a new command by generating a unique identifier and assigning
  * it to the current command state {@code Dispatcher.prototype.commandId}.
  *
@@ -287,12 +228,11 @@ Dispatcher.prototype.beginNewCommand = f
   return uuid;
 };
 
 Dispatcher.prototype.isOutOfSync = function(cmdId) {
   return this.commandId !== cmdId;
 };
 
 Dispatcher.prototype.requests = {
-  getMarionetteID: Dispatcher.prototype.getMarionetteID,
   emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult,
   quitApplication: Dispatcher.prototype.quitApplication
 };
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -568,33 +568,33 @@ GeckoDriver.prototype.newSession = funct
     }, BROWSER_STARTUP_FINISHED, false);
   } else {
     runSessionStart.call(this);
   }
 
   yield registerBrowsers;
   yield browserListening;
 
-  resp.sessionId = this.sessionId;
-  resp.value = this.sessionCapabilities;
+  resp.body.sessionId = this.sessionId;
+  resp.body.capabilities = this.sessionCapabilities;
 };
 
 /**
  * Send the current session's capabilities to the client.
  *
  * Capabilities informs the client of which WebDriver features are
  * supported by Firefox and Marionette.  They are immutable for the
  * length of the session.
  *
  * The return value is an immutable map of string keys
  * ("capabilities") to values, which may be of types boolean,
  * numerical or string.
  */
 GeckoDriver.prototype.getSessionCapabilities = function(cmd, resp) {
-  resp.value = this.sessionCapabilities;
+  resp.body.capabilities = this.sessionCapabilities;
 };
 
 /**
  * Update the sessionCapabilities object with the keys that have been
  * passed in when a new session is created.
  *
  * This is not a public API, only available when a new session is
  * created.
@@ -716,17 +716,17 @@ GeckoDriver.prototype.setUpProxy = funct
  *     Arbitrary log level.
  */
 GeckoDriver.prototype.log = function(cmd, resp) {
   this.marionetteLog.log(cmd.parameters.value, cmd.parameters.level);
 };
 
 /** Return all logged messages. */
 GeckoDriver.prototype.getLogs = function(cmd, resp) {
-  resp.value = this.marionetteLog.getLogs();
+  resp.body = this.marionetteLog.getLogs();
 };
 
 /**
  * Sets the context of the subsequent commands to be either "chrome" or
  * "content".
  *
  * @param {string} value
  *     Name of the context to be switched to.  Must be one of "chrome" or
@@ -738,17 +738,17 @@ GeckoDriver.prototype.setContext = funct
   if (ctx === null) {
     throw new WebDriverError(`Invalid context: ${val}`);
   }
   this.context = ctx;
 };
 
 /** Gets the context of the server, either "chrome" or "content". */
 GeckoDriver.prototype.getContext = function(cmd, resp) {
-  resp.value = this.context.toString();
+  resp.body.value = this.context.toString();
 };
 
 /**
  * Returns a chrome sandbox that can be used by the execute and
  * executeWithCallback functions.
  *
  * @param {nsIDOMWindow} win
  *     Window in which we will execute code.
@@ -839,17 +839,17 @@ GeckoDriver.prototype.executeScriptInSan
       (typeof res == "undefined" || typeof res.passed == "undefined")) {
     throw new WebDriverError("finish() not called");
   }
 
   if (!async) {
     // It's fine to pass on and modify resp here because
     // executeScriptInSandbox is the last function to be called
     // in execute and executeWithCallback respectively.
-    resp.value = this.curBrowser.elementManager.wrapValue(res);
+    resp.body.value = this.curBrowser.elementManager.wrapValue(res);
   }
 };
 
 /**
  * Execute the given script either as a function body or directly (for
  * mochitest-like JS Marionette tests).
  *
  * If directInject is ture, it will run directly and not as a function
@@ -868,17 +868,17 @@ GeckoDriver.prototype.execute = function
   if (!scriptTimeout) {
     scriptTimeout = this.scriptTimeout;
   }
   if (typeof newSandbox == "undefined") {
     newSandbox = true;
   }
 
   if (this.context == Context.CONTENT) {
-    resp.value = yield this.listener.executeScript({
+    resp.body.value = yield this.listener.executeScript({
       script: script,
       args: args,
       newSandbox: newSandbox,
       timeout: scriptTimeout,
       filename: filename,
       line: line,
       sandboxName: sandboxName
     });
@@ -981,17 +981,17 @@ GeckoDriver.prototype.executeJSScript = 
       if (cmd.parameters.async) {
         yield this.executeWithCallback(cmd, resp, cmd.parameters.async);
       } else {
         this.execute(cmd, resp, true /* async */);
       }
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.executeJSScript({
+      resp.body.value = yield this.listener.executeJSScript({
         script: cmd.parameters.script,
         args: cmd.parameters.args,
         newSandbox: cmd.parameters.newSandbox,
         async: cmd.parameters.async,
         timeout: cmd.parameters.scriptTimeout ?
             cmd.parameters.scriptTimeout : this.scriptTimeout,
         inactivityTimeout: cmd.parameters.inactivityTimeout,
         filename: cmd.parameters.filename,
@@ -1020,27 +1020,27 @@ GeckoDriver.prototype.executeJSScript = 
 GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
   let {script,
       args,
       newSandbox,
       inactivityTimeout,
       scriptTimeout,
       filename,
       line} = cmd.parameters;
-  let sandboxName = cmd.parameters.sandbox || 'default';
+  let sandboxName = cmd.parameters.sandbox || "default";
 
   if (!scriptTimeout) {
     scriptTimeout = this.scriptTimeout;
   }
   if (typeof newSandbox == "undefined") {
     newSandbox = true;
   }
 
   if (this.context == Context.CONTENT) {
-    resp.value = yield this.listener.executeAsyncScript({
+    resp.body.value = yield this.listener.executeAsyncScript({
       script: script,
       args: args,
       id: cmd.id,
       newSandbox: newSandbox,
       timeout: scriptTimeout,
       inactivityTimeout: inactivityTimeout,
       filename: filename,
       line: line,
@@ -1179,17 +1179,17 @@ GeckoDriver.prototype.executeWithCallbac
           directInject,
           true /* async */,
           scriptTimeout);
     } catch (e) {
       chromeAsyncError(e, "execute_async_script", filename, line, script);
     }
   }.bind(this));
 
-  resp.value = that.curBrowser.elementManager.wrapValue(res);
+  resp.body.value = that.curBrowser.elementManager.wrapValue(res) || null;
 };
 
 /**
  * Navigate to given URL.
  *
  * Navigates the current browsing context to the given URL and waits for
  * the document to load or the session's page timeout duration to elapse
  * before returning.
@@ -1277,60 +1277,60 @@ GeckoDriver.prototype.pageLoadPromise = 
  *
  * On Desktop this returns a string representation of the URL of the
  * current top level browsing context.  This is equivalent to
  * document.location.href.
  *
  * When in the context of the chrome, this returns the canonical URL
  * of the current resource.
  */
-GeckoDriver.prototype.getCurrentUrl = function(cmd, resp) {
+GeckoDriver.prototype.getCurrentUrl = function(cmd) {
   switch (this.context) {
     case Context.CHROME:
-      resp.value = this.getCurrentWindow().location.href;
+      return this.getCurrentWindow().location.href;
       break;
 
     case Context.CONTENT:
       let isB2G = this.appName == "B2G";
-      resp.value = yield this.listener.getCurrentUrl(isB2G);
+      return this.listener.getCurrentUrl(isB2G);
       break;
   }
 };
 
 /** Gets the current title of the window. */
 GeckoDriver.prototype.getTitle = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      resp.value = win.document.documentElement.getAttribute("title");
+      resp.body.value = win.document.documentElement.getAttribute("title");
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getTitle();
+      resp.body.value = yield this.listener.getTitle();
       break;
   }
 };
 
 /** Gets the current type of the window. */
 GeckoDriver.prototype.getWindowType = function(cmd, resp) {
   let win = this.getCurrentWindow();
-  resp.value = win.document.documentElement.getAttribute("windowtype");
+  resp.body.value = win.document.documentElement.getAttribute("windowtype");
 };
 
 /** Gets the page source of the content document. */
 GeckoDriver.prototype.getPageSource = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
       let s = new win.XMLSerializer();
-      resp.value = s.serializeToString(win.document);
+      resp.body.value = s.serializeToString(win.document);
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getPageSource();
+      resp.body.value = yield this.listener.getPageSource();
       break;
   }
 };
 
 /** Go back in history. */
 GeckoDriver.prototype.goBack = function(cmd, resp) {
   yield this.listener.goBack();
 };
@@ -1354,23 +1354,23 @@ GeckoDriver.prototype.refresh = function
  * be used to switch to this window at a later point.
  *
  * @return {string}
  *     Unique window handle.
  */
 GeckoDriver.prototype.getWindowHandle = function(cmd, resp) {
   // curFrameId always holds the current tab.
   if (this.curBrowser.curFrameId && this.appName != "B2G") {
-    resp.value = this.curBrowser.curFrameId;
+    resp.body.value = this.curBrowser.curFrameId;
     return;
   }
 
   for (let i in this.browsers) {
     if (this.curBrowser == this.browsers[i]) {
-      resp.value = i;
+      resp.body.value = i;
       return;
     }
   }
 };
 
 /**
  * Forces an update for the given browser's id.
  */
@@ -1407,90 +1407,91 @@ GeckoDriver.prototype.getIdForBrowser = 
  *
  * Each window handle is assigned by the server and is guaranteed unique,
  * however the return array does not have a specified ordering.
  *
  * @return {Array.<string>}
  *     Unique window handles.
  */
 GeckoDriver.prototype.getWindowHandles = function(cmd, resp) {
-  let rv = [];
+  let hs = [];
   let winEn = this.getWinEnumerator();
   while (winEn.hasMoreElements()) {
     let win = winEn.getNext();
     if (win.gBrowser && this.appName != "B2G") {
       let tabbrowser = win.gBrowser;
       for (let i = 0; i < tabbrowser.browsers.length; ++i) {
         let winId = this.getIdForBrowser(tabbrowser.getBrowserAtIndex(i));
         if (winId !== null) {
-          rv.push(winId);
+          hs.push(winId);
         }
       }
     } else {
       // XUL Windows, at least, do not have gBrowser
       let winId = win.QueryInterface(Ci.nsIInterfaceRequestor)
           .getInterface(Ci.nsIDOMWindowUtils)
           .outerWindowID;
       winId += (this.appName == "B2G") ? "-b2g" : "";
-      rv.push(winId);
+      hs.push(winId);
     }
   }
-  resp.value = rv;
+  resp.body = hs;
 };
 
 /**
  * Get the current window's handle.  This corresponds to a window that
  * may itself contain tabs.
  *
  * Return an opaque server-assigned identifier to this window that
  * uniquely identifies it within this Marionette instance.  This can
  * be used to switch to this window at a later point.
  *
  * @return {string}
  *     Unique window handle.
  */
 GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) {
   for (let i in this.browsers) {
     if (this.curBrowser == this.browsers[i]) {
-      resp.value = i;
+      resp.body.value = i;
       return;
     }
   }
 };
 
 /**
  * Returns identifiers for each open chrome window for tests interested in
  * managing a set of chrome windows and tabs separately.
  *
  * @return {Array.<string>}
  *     Unique window handles.
  */
 GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) {
-  let rv = [];
+  let hs = [];
   let winEn = this.getWinEnumerator();
   while (winEn.hasMoreElements()) {
     let foundWin = winEn.getNext();
     let winId = foundWin.QueryInterface(Ci.nsIInterfaceRequestor)
         .getInterface(Ci.nsIDOMWindowUtils)
         .outerWindowID;
     winId = winId + ((this.appName == "B2G") ? "-b2g" : "");
-    rv.push(winId);
+    hs.push(winId);
   }
-  resp.value = rv;
+  resp.body = hs;
 };
 
 /**
  * Get the current window position.
  *
  * @return {Object.<string, number>}
  *     Object with x and y coordinates.
  */
 GeckoDriver.prototype.getWindowPosition = function(cmd, resp) {
   let win = this.getCurrentWindow();
-  resp.value = {x: win.screenX, y: win.screenY};
+  resp.body.x = win.screenX;
+  resp.body.y = win.screenY;
 };
 
 /**
  * Set the window position of the browser on the OS Window Manager
  *
  * @param {number} x
  *     X coordinate of the top/left of the window that it will be
  *     moved to.
@@ -1595,25 +1596,25 @@ GeckoDriver.prototype.switchToWindow = f
     throw new NoSuchWindowError(`Unable to locate window: ${switchTo}`);
   }
 };
 
 GeckoDriver.prototype.getActiveFrame = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
       // no frame means top-level
-      resp.value = null;
+      resp.body.value = null;
       if (this.curFrame) {
-        resp.value = this.curBrowser.elementManager
+        resp.body.value = this.curBrowser.elementManager
             .addToKnownElements(this.curFrame.frameElement);
       }
       break;
 
     case Context.CONTENT:
-      resp.value = this.currentFrameElement;
+      resp.body.value = this.currentFrameElement;
       break;
   }
 };
 
 /**
  * Switch to a given frame within the current window.
  *
  * @param {Object} element
@@ -1828,27 +1829,27 @@ GeckoDriver.prototype.actionChain = func
       if (this.appName != "Firefox") {
         // be conservative until this has a use case and is established
         // to work as expected on b2g/fennec
         throw new WebDriverError(
             "Command 'actionChain' is not available in chrome context");
       }
 
       let cbs = {};
-      cbs.onSuccess = val => resp.value = val;
+      cbs.onSuccess = val => resp.body.value = val;
       cbs.onError = err => { throw err; };
 
       let win = this.getCurrentWindow();
       let elm = this.curBrowser.elementManager;
       this.actions.dispatchActions(chain, nextId, { frame: win }, elm, cbs);
       break;
 
     case Context.CONTENT:
       this.addFrameCloseListener("action chain");
-      resp.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
+      resp.body.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
       break;
   }
 };
 
 /**
  * A multi-action chain.
  *
  * @param {Object} value
@@ -1875,30 +1876,30 @@ GeckoDriver.prototype.multiAction = func
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElement = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
-      resp.value = yield new Promise((resolve, reject) => {
+      resp.body.value = yield new Promise((resolve, reject) => {
         let win = this.getCurrentWindow();
         this.curBrowser.elementManager.find(
             { frame: win },
             cmd.parameters,
             this.searchTimeout,
             false /* all */,
             resolve,
             reject);
       }).then(null, e => { throw e; });
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.findElementContent({
+      resp.body.value = yield this.listener.findElementContent({
         value: cmd.parameters.value,
         using: cmd.parameters.using,
         element: cmd.parameters.element,
         searchTimeout: this.searchTimeout});
       break;
   }
 };
 
@@ -1909,17 +1910,17 @@ GeckoDriver.prototype.findElement = func
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
  * @param {string} id
  *     Value of the element to start from.
  */
 GeckoDriver.prototype.findChildElement = function(cmd, resp) {
-  resp.value = yield this.listener.findElementContent({
+  resp.body.value = yield this.listener.findElementContent({
     value: cmd.parameters.value,
     using: cmd.parameters.using,
     element: cmd.parameters.id,
     searchTimeout: this.searchTimeout});
 };
 
 /**
  * Find elements using the indicated search strategy.
@@ -1927,60 +1928,60 @@ GeckoDriver.prototype.findChildElement =
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
  */
 GeckoDriver.prototype.findElements = function(cmd, resp) {
   switch (this.context) {
     case Context.CHROME:
-      resp.value = yield new Promise((resolve, reject) => {
+      resp.body = yield new Promise((resolve, reject) => {
         let win = this.getCurrentWindow();
         this.curBrowser.elementManager.find(
             { frame: win },
             cmd.parameters,
             this.searchTimeout,
             true /* all */,
             resolve,
             reject);
       }).then(null, e => { throw new NoSuchElementError(e.message); });
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.findElementsContent({
-          value: cmd.parameters.value,
-          using: cmd.parameters.using,
-          element: cmd.parameters.element,
-          searchTimeout: this.searchTimeout});
+      resp.body = yield this.listener.findElementsContent({
+        value: cmd.parameters.value,
+        using: cmd.parameters.using,
+        element: cmd.parameters.element,
+        searchTimeout: this.searchTimeout});
       break;
   }
 };
 
 /**
  * Find elements using the indicated search strategy starting from a
  * known element.  Used for WebDriver Compatibility only.
  *
  * @param {string} using
  *     Indicates which search method to use.
  * @param {string} value
  *     Value the client is looking for.
  * @param {string} id
  *     Value of the element to start from.
  */
 GeckoDriver.prototype.findChildElements = function(cmd, resp) {
-  resp.value = yield this.listener.findElementsContent({
+  resp.body.value = yield this.listener.findElementsContent({
     value: cmd.parameters.value,
     using: cmd.parameters.using,
     element: cmd.parameters.id,
     searchTimeout: this.searchTimeout});
 };
 
 /** Return the active element on the page. */
 GeckoDriver.prototype.getActiveElement = function(cmd, resp) {
-  resp.value = yield this.listener.getActiveElement();
+  resp.body.value = yield this.listener.getActiveElement();
 };
 
 /**
  * Send click event to element.
  *
  * @param {string} id
  *     Reference ID to the element that will be clicked.
  */
@@ -2015,22 +2016,22 @@ GeckoDriver.prototype.clickElement = fun
  *     Name of the attribute to retrieve.
  */
 GeckoDriver.prototype.getElementAttribute = function(cmd, resp) {
   let {id, name} = cmd.parameters;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
-      resp.value = utils.getElementAttribute(el, name);
+      let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
+      resp.body.value = utils.getElementAttribute(el, name);
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getElementAttribute(id, name);
+      resp.body.value = yield this.listener.getElementAttribute(id, name);
       break;
   }
 };
 
 /**
  * Get the text of an element, if any.  Includes the text of all child
  * elements.
  *
@@ -2042,65 +2043,65 @@ GeckoDriver.prototype.getElementText = f
 
   switch (this.context) {
     case Context.CHROME:
       // for chrome, we look at text nodes, and any node with a "label" field
       let win = this.getCurrentWindow();
       let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let lines = [];
       this.getVisibleText(el, lines);
-      resp.value = lines.join("\n");
+      resp.body.value = lines.join("\n");
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getElementText(id);
+      resp.body.value = yield this.listener.getElementText(id);
       break;
   }
 };
 
 /**
  * Get the tag name of the element.
  *
  * @param {string} id
  *     Reference ID to the element that will be inspected.
  */
 GeckoDriver.prototype.getElementTagName = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
-      resp.value = el.tagName.toLowerCase();
+      let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
+      resp.body.value = el.tagName.toLowerCase();
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getElementTagName(id);
+      resp.body.value = yield this.listener.getElementTagName(id);
       break;
   }
 };
 
 /**
  * Check if element is displayed.
  *
  * @param {string} id
  *     Reference ID to the element that will be inspected.
  */
 GeckoDriver.prototype.isElementDisplayed = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
-      resp.value = utils.isElementDisplayed(el);
+      let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
+      resp.body.value = utils.isElementDisplayed(el);
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.isElementDisplayed(id);
+      resp.body.value = yield this.listener.isElementDisplayed(id);
       break;
   }
 };
 
 /**
  * Return the property of the computed style of an element.
  *
  * @param {string} id
@@ -2111,21 +2112,21 @@ GeckoDriver.prototype.isElementDisplayed
 GeckoDriver.prototype.getElementValueOfCssProperty = function(cmd, resp) {
   let {id, propertyName: prop} = cmd.parameters;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
       let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let sty = win.document.defaultView.getComputedStyle(el, null);
-      resp.value = sty.getPropertyValue(prop);
+      resp.body.value = sty.getPropertyValue(prop);
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getElementValueOfCssProperty(id, prop);
+      resp.body.value = yield this.listener.getElementValueOfCssProperty(id, prop);
       break;
   }
 };
 
 /**
  * Check if element is enabled.
  *
  * @param {string} id
@@ -2133,22 +2134,22 @@ GeckoDriver.prototype.getElementValueOfC
  */
 GeckoDriver.prototype.isElementEnabled = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
       let win = this.getCurrentWindow();
-      let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
-      resp.value = !(!!el.disabled);
+      let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win});
+      resp.body.value = !(!!el.disabled);
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.isElementEnabled(id);
+      resp.body.value = yield this.listener.isElementEnabled(id);
       break;
   }
 },
 
 /**
  * Check if element is selected.
  *
  * @param {string} id
@@ -2158,65 +2159,66 @@ GeckoDriver.prototype.isElementSelected 
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       // Selenium atom doesn't quite work here
       let win = this.getCurrentWindow();
       let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       if (typeof el.checked != "undefined") {
-        resp.value = !!el.checked;
+        resp.body.value = !!el.checked;
       } else if (typeof el.selected != "undefined") {
-        resp.value = !!el.selected;
+        resp.body.value = !!el.selected;
       } else {
-        resp.value = true;
+        resp.body.value = true;
       }
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.isElementSelected(id);
+      resp.body.value = yield this.listener.isElementSelected(id);
       break;
   }
 };
 
 GeckoDriver.prototype.getElementSize = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
       let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let rect = el.getBoundingClientRect();
-      resp.value = {width: rect.width, height: rect.height};
+      resp.body.width = rect.width;
+      resp.body.height = rect.height;
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getElementSize(id);
+      resp.body = yield this.listener.getElementSize(id);
       break;
   }
 };
 
 GeckoDriver.prototype.getElementRect = function(cmd, resp) {
   let id = cmd.parameters.id;
 
   switch (this.context) {
     case Context.CHROME:
       let win = this.getCurrentWindow();
       let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win });
       let rect = el.getBoundingClientRect();
-      resp.value = {
+      resp.body = {
         x: rect.x + win.pageXOffset,
         y: rect.y + win.pageYOffset,
         width: rect.width,
         height: rect.height
       };
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.getElementRect(id);
+      resp.body = yield this.listener.getElementRect(id);
       break;
   }
 };
 
 /**
  * Send key presses to element after focusing on it.
  *
  * @param {string} id
@@ -2346,17 +2348,17 @@ GeckoDriver.prototype.addCookie = functi
 
 /**
  * Get all the cookies for the current domain.
  *
  * This is the equivalent of calling {@code document.cookie} and parsing
  * the result.
  */
 GeckoDriver.prototype.getCookies = function(cmd, resp) {
-  resp.value = yield this.listener.getCookies();
+  resp.body = yield this.listener.getCookies();
 };
 
 /** Delete all cookies that are visible to a document. */
 GeckoDriver.prototype.deleteAllCookies = function(cmd, resp) {
   yield this.listener.deleteAllCookies();
 };
 
 /** Delete a cookie by name. */
@@ -2510,17 +2512,17 @@ GeckoDriver.prototype.sessionTearDown = 
  * the session and responding "ok".
  */
 GeckoDriver.prototype.deleteSession = function(cmd, resp) {
   this.sessionTearDown();
 };
 
 /** Returns the current status of the Application Cache. */
 GeckoDriver.prototype.getAppCacheStatus = function(cmd, resp) {
-  resp.value = yield this.listener.getAppCacheStatus();
+  resp.body.value = yield this.listener.getAppCacheStatus();
 };
 
 GeckoDriver.prototype.importScript = function(cmd, resp) {
   let script = cmd.parameters.script;
 
   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
       .createInstance(Ci.nsIScriptableUnicodeConverter);
   converter.charset = "UTF-8";
@@ -2628,37 +2630,37 @@ GeckoDriver.prototype.takeScreenshot = f
         flags =
           context.DRAWWINDOW_DRAW_VIEW |
           context.DRAWWINDOW_USE_WIDGET_LAYERS;
       }
       context.scale(scale, scale);
       context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags);
       let dataUrl = canvas.toDataURL("image/png", "");
       let data = dataUrl.substring(dataUrl.indexOf(",") + 1);
-      resp.value = data;
+      resp.body.value = data;
       break;
 
     case Context.CONTENT:
-      resp.value = yield this.listener.takeScreenshot({
+      resp.body.value = yield this.listener.takeScreenshot({
         id: cmd.parameters.id,
         highlights: cmd.parameters.highlights,
         full: cmd.parameters.full});
       break;
   }
 };
 
 /**
  * Get the current browser orientation.
  *
  * Will return one of the valid primary orientation values
  * portrait-primary, landscape-primary, portrait-secondary, or
  * landscape-secondary.
  */
 GeckoDriver.prototype.getScreenOrientation = function(cmd, resp) {
-  resp.value = this.getCurrentWindow().screen.mozOrientation;
+  resp.body.value = this.getCurrentWindow().screen.mozOrientation;
 };
 
 /**
  * Set the current browser orientation.
  *
  * The supplied orientation should be given as one of the valid
  * orientation values.  If the orientation is unknown, an error will
  * be raised.
@@ -2690,17 +2692,18 @@ GeckoDriver.prototype.setScreenOrientati
  * Get the size of the browser window currently in focus.
  *
  * Will return the current browser window size in pixels. Refers to
  * window outerWidth and outerHeight values, which include scroll bars,
  * title bars, etc.
  */
 GeckoDriver.prototype.getWindowSize = function(cmd, resp) {
   let win = this.getCurrentWindow();
-  resp.value = {width: win.outerWidth, height: win.outerHeight};
+  resp.body.width = win.outerWidth;
+  resp.body.height = win.outerHeight;
 };
 
 /**
  * Set the size of the browser window currently in focus.
  *
  * Not supported on B2G. The supplied width and height values refer to
  * the window outerWidth and outerHeight values, which include scroll
  * bars, title bars, etc.
@@ -2776,17 +2779,17 @@ GeckoDriver.prototype.acceptDialog = fun
  */
 GeckoDriver.prototype.getTextFromDialog = function(cmd, resp) {
   if (!this.dialog) {
     throw new NoAlertOpenError(
         "No tab modal was open when attempting to get the dialog text");
   }
 
   let {infoBody} = this.dialog.ui;
-  resp.value = infoBody.textContent;
+  resp.body.value = infoBody.textContent;
 };
 
 /**
  * Sends keys to the input field of a currently displayed modal, or
  * returns a no such alert error if no modal is currently displayed. If
  * a tab modal is currently displayed but has no means for text input,
  * an element not visible error is returned.
  */
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -49,42 +49,16 @@ const XPCOM_EXCEPTIONS = [];
   for (let prop in Cr) {
     XPCOM_EXCEPTIONS.push(Cr[prop]);
   }
 }
 
 this.error = {};
 
 /**
- * Marshals an error object into a WebDriver protocol error.  The given
- * error can be a prototypal Error or an object, as long as it has the
- * properties message, stack, and status.
- *
- * If err is a native JavaScript error, the returned object's message
- * property will be changed to include the error's name.
- *
- * @param {Object} err
- *     Object with the properties message, stack, and status.
- *
- * @return {Object}
- *     Object with the properties message, stacktrace, and status.
- */
-error.toJSON = function(err) {
-  let msg = err.message;
-  if (!error.isWebDriverError(err) && "name" in error) {
-    msg = `${err.name}: ${msg}`;
-  }
-  return {
-    message: msg,
-    stacktrace: err.stack || null,
-    status: err.status
-  };
-};
-
-/**
  * Determines if the given status is successful.
  */
 error.isSuccess = status => status === "success";
 
 /**
  * Checks if obj is an instance of the Error prototype in a safe manner.
  * Prefer using this over using instanceof since the Error prototype
  * isn't unique across browsers, and XPCOM exceptions are special
@@ -105,17 +79,17 @@ error.isError = function(val) {
   }
 };
 
 /**
  * Checks if obj is an object in the WebDriverError prototypal chain.
  */
 error.isWebDriverError = function(obj) {
   return error.isError(obj) &&
-      ("name" in obj && errors.indexOf(obj.name) > 0);
+      ("name" in obj && errors.indexOf(obj.name) >= 0);
 };
 
 /**
  * Unhandled error reporter.  Dumps the error and its stacktrace to console,
  * and reports error to the Browser Console.
  */
 error.report = function(err) {
   let msg = `Marionette threw an error: ${error.stringify(err)}`;
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -208,16 +208,17 @@ let getCurrentUrlFn = dispatch(getCurren
 let findElementContentFn = dispatch(findElementContent);
 let findElementsContentFn = dispatch(findElementsContent);
 let isElementSelectedFn = dispatch(isElementSelected);
 let getElementLocationFn = dispatch(getElementLocation);
 let clearElementFn = dispatch(clearElement);
 let isElementDisplayedFn = dispatch(isElementDisplayed);
 let getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty);
 let switchToShadowRootFn = dispatch(switchToShadowRoot);
+let getCookiesFn = dispatch(getCookies);
 
 /**
  * Start all message listeners
  */
 function startListeners() {
   addMessageListenerId("Marionette:receiveFiles", receiveFiles);
   addMessageListenerId("Marionette:newSession", newSession);
   addMessageListenerId("Marionette:executeScript", executeScript);
@@ -256,17 +257,17 @@ function startListeners() {
   addMessageListenerId("Marionette:deleteSession", deleteSession);
   addMessageListenerId("Marionette:sleepSession", sleepSession);
   addMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   addMessageListenerId("Marionette:importScript", importScript);
   addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   addMessageListenerId("Marionette:setTestName", setTestName);
   addMessageListenerId("Marionette:takeScreenshot", takeScreenshot);
   addMessageListenerId("Marionette:addCookie", addCookie);
-  addMessageListenerId("Marionette:getCookies", getCookies);
+  addMessageListenerId("Marionette:getCookies", getCookiesFn);
   addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies);
   addMessageListenerId("Marionette:deleteCookie", deleteCookie);
 }
 
 /**
  * Used during newSession and restart, called to set up the modal dialog listener in b2g
  */
 function waitForReady() {
@@ -362,17 +363,17 @@ function deleteSession(msg) {
   removeMessageListenerId("Marionette:deleteSession", deleteSession);
   removeMessageListenerId("Marionette:sleepSession", sleepSession);
   removeMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult);
   removeMessageListenerId("Marionette:importScript", importScript);
   removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
   removeMessageListenerId("Marionette:setTestName", setTestName);
   removeMessageListenerId("Marionette:takeScreenshot", takeScreenshot);
   removeMessageListenerId("Marionette:addCookie", addCookie);
-  removeMessageListenerId("Marionette:getCookies", getCookies);
+  removeMessageListenerId("Marionette:getCookies", getCookiesFn);
   removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies);
   removeMessageListenerId("Marionette:deleteCookie", deleteCookie);
   if (isB2G) {
     content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false);
   }
   elementManager.reset();
   // reset container frame to the top-most frame
   curContainer = { frame: content, shadowRoot: null };
@@ -1873,37 +1874,41 @@ function addCookie(msg) {
     return;
   }
   sendOk(msg.json.command_id);
 }
 
 /**
  * Get all cookies for the current domain.
  */
-function getCookies(msg) {
-  var toReturn = [];
-  var cookies = getVisibleCookies(curContainer.frame.location);
+function getCookies() {
+  let rv = [];
+  let cookies = getVisibleCookies(curContainer.frame.location);
+
   for (let cookie of cookies) {
-    var expires = cookie.expires;
-    if (expires == 0) {  // Session cookie, don't return an expiry.
+    let expires = cookie.expires;
+    // session cookie, don't return an expiry
+    if (expires == 0) {
       expires = null;
-    } else if (expires == 1) { // Date before epoch time, cap to epoch.
+    // date before epoch time, cap to epoch
+    } else if (expires == 1) {
       expires = 0;
     }
-    toReturn.push({
+    rv.push({
       'name': cookie.name,
       'value': cookie.value,
       'path': cookie.path,
       'domain': cookie.host,
       'secure': cookie.isSecure,
       'httpOnly': cookie.httpOnly,
       'expiry': expires
     });
   }
-  sendResponse({value: toReturn}, msg.json.command_id);
+
+  return rv;
 }
 
 /**
  * Delete a cookie by name
  */
 function deleteCookie(msg) {
   let toDelete = msg.json.name;
   let cookies = getVisibleCookies(curContainer.frame.location);