Bug 739173 - Support FormData in workers. r=khuey,baku
authorNikhil Marathe <nsm.nikhil@gmail.com>
Tue, 27 Jan 2015 15:16:21 -0800
changeset 264313 10b88a94ab27affa0d57182805ab1f2af65220b0
parent 264312 ddd0d3a6a1ae3d13e9a5b9514a14f8bdb1d76a33
child 264314 a80676087e0ab40748ae6c5d0b18ee93ad1684f1
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskhuey, baku
bugs739173
milestone39.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 739173 - Support FormData in workers. r=khuey,baku
dom/base/nsFormData.h
dom/html/test/formData_test.js
dom/html/test/formData_worker.js
dom/html/test/mochitest.ini
dom/html/test/test_formData.html
dom/webidl/FormData.webidl
dom/workers/WorkerPrivate.cpp
dom/workers/WorkerPrivate.h
dom/workers/XMLHttpRequest.cpp
dom/workers/XMLHttpRequest.h
dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
dom/workers/test/test_worker_interfaces.js
testing/web-platform/meta/workers/semantics/interface-objects/001.worker.js.ini
--- a/dom/base/nsFormData.h
+++ b/dom/base/nsFormData.h
@@ -115,15 +115,42 @@ public:
   }
   virtual nsresult AddNameFilePair(const nsAString& aName,
                                    File* aBlob) MOZ_OVERRIDE
   {
     FormDataTuple* data = mFormData.AppendElement();
     SetNameFilePair(data, aName, aBlob);
     return NS_OK;
   }
+
+  typedef bool (*FormDataEntryCallback)(const nsString& aName, bool aIsFile,
+                                        const nsString& aValue,
+                                        File* aFile, void* aClosure);
+
+  uint32_t
+  Length() const
+  {
+    return mFormData.Length();
+  }
+
+  // Stops iteration and returns false if any invocation of callback returns
+  // false. Returns true otherwise.
+  bool
+  ForEach(FormDataEntryCallback aFunc, void* aClosure)
+  {
+    for (uint32_t i = 0; i < mFormData.Length(); ++i) {
+      FormDataTuple& tuple = mFormData[i];
+      if (!aFunc(tuple.name, tuple.valueIsFile, tuple.stringValue,
+                 tuple.fileValue, aClosure)) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
 private:
   nsCOMPtr<nsISupports> mOwner;
 
   nsTArray<FormDataTuple> mFormData;
 };
 
 #endif // nsFormData_h__
new file mode 100644
--- /dev/null
+++ b/dom/html/test/formData_test.js
@@ -0,0 +1,167 @@
+function testHas() {
+  var f = new FormData();
+  f.append("foo", "bar");
+  f.append("another", "value");
+  ok(f.has("foo"), "has() on existing name should be true.");
+  ok(f.has("another"), "has() on existing name should be true.");
+  ok(!f.has("nonexistent"), "has() on non-existent name should be false.");
+}
+
+function testGet() {
+  var f = new FormData();
+  f.append("foo", "bar");
+  f.append("foo", "bar2");
+  f.append("blob", new Blob(["hey"], { type: 'text/plain' }));
+  f.append("file", new File(["hey"], 'testname',  {type: 'text/plain'}));
+
+  is(f.get("foo"), "bar", "get() on existing name should return first value");
+  ok(f.get("blob") instanceof Blob, "get() on existing name should return first value");
+  is(f.get("blob").type, 'text/plain', "get() on existing name should return first value");
+  ok(f.get("file") instanceof File, "get() on existing name should return first value");
+  is(f.get("file").name, 'testname', "get() on existing name should return first value");
+
+  is(f.get("nonexistent"), null, "get() on non-existent name should return null.");
+}
+
+function testGetAll() {
+  var f = new FormData();
+  f.append("other", "value");
+  f.append("foo", "bar");
+  f.append("foo", "bar2");
+  f.append("foo", new Blob(["hey"], { type: 'text/plain' }));
+
+  var arr = f.getAll("foo");
+  is(arr.length, 3, "getAll() should retrieve all matching entries.");
+  is(arr[0], "bar", "values should match and be in order");
+  is(arr[1], "bar2", "values should match and be in order");
+  ok(arr[2] instanceof Blob, "values should match and be in order");
+
+  is(f.get("nonexistent"), null, "get() on non-existent name should return null.");
+}
+
+function testDelete() {
+  var f = new FormData();
+  f.append("other", "value");
+  f.append("foo", "bar");
+  f.append("foo", "bar2");
+  f.append("foo", new Blob(["hey"], { type: 'text/plain' }));
+
+  ok(f.has("foo"), "has() on existing name should be true.");
+  f.delete("foo");
+  ok(!f.has("foo"), "has() on deleted name should be false.");
+  is(f.getAll("foo").length, 0, "all entries should be deleted.");
+
+  is(f.getAll("other").length, 1, "other names should still be there.");
+  f.delete("other");
+  is(f.getAll("other").length, 0, "all entries should be deleted.");
+}
+
+function testSet() {
+  var f = new FormData();
+
+  f.set("other", "value");
+  ok(f.has("other"), "set() on new name should be similar to append()");
+  is(f.getAll("other").length, 1, "set() on new name should be similar to append()");
+
+  f.append("other", "value2");
+  is(f.getAll("other").length, 2, "append() should not replace existing entries.");
+
+  f.append("foo", "bar");
+  f.append("other", "value3");
+  f.append("other", "value3");
+  f.append("other", "value3");
+  is(f.getAll("other").length, 5, "append() should not replace existing entries.");
+
+  f.set("other", "value4");
+  is(f.getAll("other").length, 1, "set() should replace existing entries.");
+  is(f.getAll("other")[0], "value4", "set() should replace existing entries.");
+}
+
+function testIterate() {
+  todo(false, "Implement this in Bug 1085284.");
+}
+
+function testFilename() {
+  var f = new FormData();
+  // Spec says if a Blob (which is not a File) is added, the name parameter is set to "blob".
+  f.append("blob", new Blob(["hi"]));
+  is(f.get("blob").name, "blob", "Blob's filename should be blob.");
+
+  // If a filename is passed, that should replace the original.
+  f.append("blob2", new Blob(["hi"]), "blob2.txt");
+  is(f.get("blob2").name, "blob2.txt", "Explicit filename should override \"blob\".");
+
+  var file = new File(["hi"], "file1.txt");
+  f.append("file1", file);
+  // If a file is passed, the "create entry" algorithm should not create a new File, but reuse the existing one.
+  is(f.get("file1"), file, "Retrieved File object should be original File object and not a copy.");
+  is(f.get("file1").name, "file1.txt", "File's filename should be original's name if no filename is explicitly passed.");
+
+  file = new File(["hi"], "file2.txt");
+  f.append("file2", file, "fakename.txt");
+  ok(f.get("file2") !== file, "Retrieved File object should be new File object if explicit filename is passed.");
+  is(f.get("file2").name, "fakename.txt", "File's filename should be explicitly passed name.");
+  f.append("file3", new File(["hi"], ""));
+  is(f.get("file3").name, "", "File's filename is returned even if empty.");
+}
+
+function testSend(doneCb) {
+  var xhr = new XMLHttpRequest();
+  xhr.open("POST", "form_submit_server.sjs");
+  xhr.onload = function () {
+    var response = xhr.response;
+
+    for (var entry of response) {
+      is(entry.body, 'hey');
+      is(entry.headers['Content-Type'], 'text/plain');
+    }
+
+    is(response[0].headers['Content-Disposition'],
+        'form-data; name="empty"; filename="blob"');
+
+    is(response[1].headers['Content-Disposition'],
+        'form-data; name="explicit"; filename="explicit-file-name"');
+
+    is(response[2].headers['Content-Disposition'],
+        'form-data; name="explicit-empty"; filename=""');
+
+    is(response[3].headers['Content-Disposition'],
+        'form-data; name="file-name"; filename="testname"');
+
+    is(response[4].headers['Content-Disposition'],
+        'form-data; name="empty-file-name"; filename=""');
+
+    is(response[5].headers['Content-Disposition'],
+        'form-data; name="file-name-overwrite"; filename="overwrite"');
+
+    doneCb();
+  }
+
+  var file, blob = new Blob(['hey'], {type: 'text/plain'});
+
+  var fd = new FormData();
+  fd.append("empty", blob);
+  fd.append("explicit", blob, "explicit-file-name");
+  fd.append("explicit-empty", blob, "");
+  file = new File([blob], 'testname',  {type: 'text/plain'});
+  fd.append("file-name", file);
+  file = new File([blob], '',  {type: 'text/plain'});
+  fd.append("empty-file-name", file);
+  file = new File([blob], 'testname',  {type: 'text/plain'});
+  fd.append("file-name-overwrite", file, "overwrite");
+  xhr.responseType = 'json';
+  xhr.send(fd);
+}
+
+function runTest(doneCb) {
+  testHas();
+  testGet();
+  testGetAll();
+  testDelete();
+  testSet();
+  testIterate();
+  testFilename();
+  // Finally, send an XHR and verify the response matches.
+  testSend(doneCb);
+}
+
new file mode 100644
--- /dev/null
+++ b/dom/html/test/formData_worker.js
@@ -0,0 +1,19 @@
+function ok(a, msg) {
+  postMessage({type: 'status', status: !!a, msg: a + ": " + msg });
+}
+
+function is(a, b, msg) {
+  postMessage({type: 'status', status: a === b, msg: a + " === " + b + ": " + msg });
+}
+
+function todo(a, msg) {
+  postMessage({type: 'todo', status: !!a, msg: a + ": " + msg });
+}
+
+importScripts("formData_test.js");
+
+onmessage = function() {
+  runTest(function() {
+    postMessage({type: 'finish'});
+  });
+}
--- a/dom/html/test/mochitest.ini
+++ b/dom/html/test/mochitest.ini
@@ -174,16 +174,18 @@ support-files =
   file_imports_redirect.html
   file_imports_redirect.html^headers^
   file_imports_redirected.html
   file_srcdoc-2.html
   file_srcdoc.html
   file_window_open_close_outer.html
   file_window_open_close_inner.html
   form_submit_server.sjs
+  formData_worker.js
+  formData_test.js
   image.png
   image-allow-credentials.png
   image-allow-credentials.png^headers^
   nnc_lockup.gif
   reflect.js
   wakelock.ogg
   wakelock.ogv
   file_ignoreuserfocus.html
--- a/dom/html/test/test_formData.html
+++ b/dom/html/test/test_formData.html
@@ -1,186 +1,50 @@
 <!DOCTYPE HTML>
 <html>
 <!--
 https://bugzilla.mozilla.org/show_bug.cgi?id=690659
 -->
 <head>
-  <title>Test for Bug 690659</title>
+  <title>Test for Bug 690659 and 739173</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
 </head>
 <body>
-<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=690659">Mozilla Bug 690659</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=690659">Mozilla Bug 690659 & 739173</a>
+<script type="text/javascript" src="./formData_test.js"></script>
 <script type="text/javascript">
 SimpleTest.waitForExplicitFinish();
 
-function testHas() {
-  var f = new FormData();
-  f.append("foo", "bar");
-  f.append("another", "value");
-  ok(f.has("foo"), "has() on existing name should be true.");
-  ok(f.has("another"), "has() on existing name should be true.");
-  ok(!f.has("nonexistent"), "has() on non-existent name should be false.");
-}
-
-function testGet() {
-  var f = new FormData();
-  f.append("foo", "bar");
-  f.append("foo", "bar2");
-  f.append("blob", new Blob(["hey"], { type: 'text/plain' }));
-  f.append("file", new File(["hey"], 'testname',  {type: 'text/plain'}));
-
-  is(f.get("foo"), "bar", "get() on existing name should return first value");
-  ok(f.get("blob") instanceof Blob, "get() on existing name should return first value");
-  is(f.get("blob").type, 'text/plain', "get() on existing name should return first value");
-  ok(f.get("file") instanceof File, "get() on existing name should return first value");
-  is(f.get("file").name, 'testname', "get() on existing name should return first value");
-
-  is(f.get("nonexistent"), null, "get() on non-existent name should return null.");
-}
-
-function testGetAll() {
-  var f = new FormData();
-  f.append("other", "value");
-  f.append("foo", "bar");
-  f.append("foo", "bar2");
-  f.append("foo", new Blob(["hey"], { type: 'text/plain' }));
-
-  var arr = f.getAll("foo");
-  is(arr.length, 3, "getAll() should retrieve all matching entries.");
-  is(arr[0], "bar", "values should match and be in order");
-  is(arr[1], "bar2", "values should match and be in order");
-  ok(arr[2] instanceof Blob, "values should match and be in order");
-
-  is(f.get("nonexistent"), null, "get() on non-existent name should return null.");
-}
+function runMainThreadAndWorker() {
+  var mt = new Promise(function(resolve) {
+    runTest(resolve);
+  });
 
-function testDelete() {
-  var f = new FormData();
-  f.append("other", "value");
-  f.append("foo", "bar");
-  f.append("foo", "bar2");
-  f.append("foo", new Blob(["hey"], { type: 'text/plain' }));
-
-  ok(f.has("foo"), "has() on existing name should be true.");
-  f.delete("foo");
-  ok(!f.has("foo"), "has() on deleted name should be false.");
-  is(f.getAll("foo").length, 0, "all entries should be deleted.");
-
-  is(f.getAll("other").length, 1, "other names should still be there.");
-  f.delete("other");
-  is(f.getAll("other").length, 0, "all entries should be deleted.");
-}
-
-function testSet() {
-  var f = new FormData();
+  var worker;
+  var w = new Promise(function(resolve) {
+    worker = new Worker("formData_worker.js");
+    worker.onmessage = function(event) {
+      if (event.data.type == 'finish') {
+        resolve();
+      } else if (event.data.type == 'status') {
+        ok(event.data.status, event.data.msg);
+      } else if (event.data.type == 'todo') {
+        todo(event.data.status, event.data.msg);
+      }
+    }
 
-  f.set("other", "value");
-  ok(f.has("other"), "set() on new name should be similar to append()");
-  is(f.getAll("other").length, 1, "set() on new name should be similar to append()");
-
-  f.append("other", "value2");
-  is(f.getAll("other").length, 2, "append() should not replace existing entries.");
+    worker.onerror = function(event) {
+      ok(false, "Worker had an error: " + event.message + " at " + event.lineno);
+      resolve();
+    };
 
-  f.append("foo", "bar");
-  f.append("other", "value3");
-  f.append("other", "value3");
-  f.append("other", "value3");
-  is(f.getAll("other").length, 5, "append() should not replace existing entries.");
+    worker.postMessage(true);
+  });
 
-  f.set("other", "value4");
-  is(f.getAll("other").length, 1, "set() should replace existing entries.");
-  is(f.getAll("other")[0], "value4", "set() should replace existing entries.");
-}
-
-function testIterate() {
-  todo(false, "Implement this in Bug 1085284.");
+  return Promise.all([mt, w]);
 }
 
-function testFilename() {
-  var f = new FormData();
-  // Spec says if a Blob (which is not a File) is added, the name parameter is set to blob.
-  f.append("blob", new Blob(["hi"]));
-  is(f.get("blob").name, "blob", "Blob's filename should be blob.");
-
-  // If a filename is passed, that should replace the original.
-  f.append("blob2", new Blob(["hi"]), "blob2.txt");
-  is(f.get("blob2").name, "blob2.txt", "Explicit filename should override \"blob\".");
-
-  var file = new File(["hi"], "file1.txt");
-  f.append("file1", file);
-  // If a file is passed, the "create entry" algorithm should not create a new File, but reuse the existing one.
-  is(f.get("file1"), file, "Retrieved File object should be original File object and not a copy.");
-  is(f.get("file1").name, "file1.txt", "File's filename should be original's name if no filename is explicitly passed.");
-  file = new File(["hi"], "file2.txt");
-  f.append("file2", file, "fakename.txt");
-  ok(f.get("file2") !== file, "Retrieved File object should be new File object if explicit filename is passed.");
-  is(f.get("file2").name, "fakename.txt", "File's filename should be explicitly passed name.");
-  f.append("file3", new File(["hi"], ""));
-  is(f.get("file3").name, "", "File's filename is returned even if empty.");
-}
-
-function testSend() {
-    var xhr = new XMLHttpRequest();
-    xhr.open("POST", "form_submit_server.sjs");
-    xhr.onload = function () {
-        var response = xhr.response;
-
-        for (var entry of response) {
-            is(entry.body, 'hey');
-            is(entry.headers['Content-Type'], 'text/plain');
-        }
-
-        is(response[0].headers['Content-Disposition'],
-            'form-data; name="empty"; filename="blob"');
-
-        is(response[1].headers['Content-Disposition'],
-            'form-data; name="explicit"; filename="explicit-file-name"');
-
-        is(response[2].headers['Content-Disposition'],
-            'form-data; name="explicit-empty"; filename=""');
-
-        is(response[3].headers['Content-Disposition'],
-            'form-data; name="file-name"; filename="testname"');
-
-        is(response[4].headers['Content-Disposition'],
-            'form-data; name="empty-file-name"; filename=""');
-
-        is(response[5].headers['Content-Disposition'],
-            'form-data; name="file-name-overwrite"; filename="overwrite"');
-
-        SimpleTest.finish();
-    }
-
-    var file, blob = new Blob(['hey'], {type: 'text/plain'});
-
-    var fd = new FormData();
-    fd.append("empty", blob);
-    fd.append("explicit", blob, "explicit-file-name");
-    fd.append("explicit-empty", blob, "");
-    file = new File([blob], 'testname',  {type: 'text/plain'});
-    fd.append("file-name", file);
-    file = new File([blob], '',  {type: 'text/plain'});
-    fd.append("empty-file-name", file);
-    file = new File([blob], 'testname',  {type: 'text/plain'});
-    fd.append("file-name-overwrite", file, "overwrite");
-    xhr.responseType = 'json';
-    xhr.send(fd);
-}
-
-function runTest() {
-  testHas();
-  testGet();
-  testGetAll();
-  testDelete();
-  testSet();
-  testIterate();
-  testFilename();
-  // Finally, send an XHR and verify the response matches.
-  testSend();
-}
-
-runTest()
+runMainThreadAndWorker().then(SimpleTest.finish);
 </script>
 </body>
 </html>
--- a/dom/webidl/FormData.webidl
+++ b/dom/webidl/FormData.webidl
@@ -4,17 +4,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  *
  * The origin of this IDL file is
  * http://xhr.spec.whatwg.org
  */
 
 typedef (File or USVString) FormDataEntryValue;
 
-[Constructor(optional HTMLFormElement form)]
+[Constructor(optional HTMLFormElement form),
+ Exposed=(Window,Worker)]
 interface FormData {
   void append(USVString name, Blob value, optional USVString filename);
   void append(USVString name, USVString value);
   void delete(USVString name);
   FormDataEntryValue? get(USVString name);
   sequence<FormDataEntryValue> getAll(USVString name);
   boolean has(USVString name);
   void set(USVString name, Blob value, optional USVString filename);
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -51,31 +51,33 @@
 #include "mozilla/dom/ImageData.h"
 #include "mozilla/dom/ImageDataBinding.h"
 #include "mozilla/dom/MessageEvent.h"
 #include "mozilla/dom/MessageEventBinding.h"
 #include "mozilla/dom/MessagePortList.h"
 #include "mozilla/dom/Promise.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/StructuredClone.h"
+#include "mozilla/dom/WebCryptoCommon.h"
 #include "mozilla/dom/WorkerBinding.h"
 #include "mozilla/dom/WorkerDebuggerGlobalScopeBinding.h"
 #include "mozilla/dom/WorkerGlobalScopeBinding.h"
 #include "mozilla/dom/indexedDB/IDBFactory.h"
 #include "mozilla/dom/ipc/BlobChild.h"
 #include "mozilla/dom/ipc/nsIRemoteBlob.h"
 #include "mozilla/ipc/BackgroundChild.h"
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/ipc/PBackgroundSharedTypes.h"
 #include "mozilla/Preferences.h"
 #include "MultipartFileImpl.h"
 #include "nsAlgorithm.h"
 #include "nsContentUtils.h"
 #include "nsError.h"
 #include "nsDOMJSUtils.h"
+#include "nsFormData.h"
 #include "nsHostObjectProtocolHandler.h"
 #include "nsJSEnvironment.h"
 #include "nsJSUtils.h"
 #include "nsNetUtil.h"
 #include "nsPrintfCString.h"
 #include "nsProxyRelease.h"
 #include "nsSandboxFlags.h"
 #include "prthread.h"
@@ -386,25 +388,23 @@ EnsureBlobForBackgroundManager(FileImpl*
     }
 
     MOZ_ALWAYS_TRUE(NS_SUCCEEDED(blobImpl->SetMutable(false)));
   }
 
   return blobImpl.forget();
 }
 
-void
-ReadBlobOrFile(JSContext* aCx,
-               JSStructuredCloneReader* aReader,
-               bool aIsMainThread,
-               JS::MutableHandle<JSObject*> aBlobOrFile)
+already_AddRefed<File>
+ReadBlobOrFileNoWrap(JSContext* aCx,
+                     JSStructuredCloneReader* aReader,
+                     bool aIsMainThread)
 {
   MOZ_ASSERT(aCx);
   MOZ_ASSERT(aReader);
-  MOZ_ASSERT(!aBlobOrFile);
 
   nsRefPtr<FileImpl> blobImpl;
   {
     FileImpl* rawBlobImpl;
     MOZ_ALWAYS_TRUE(JS_ReadBytes(aReader, &rawBlobImpl, sizeof(rawBlobImpl)));
 
     MOZ_ASSERT(rawBlobImpl);
 
@@ -428,19 +428,87 @@ ReadBlobOrFile(JSContext* aCx,
 
     WorkerGlobalScope* globalScope = workerPrivate->GlobalScope();
     MOZ_ASSERT(globalScope);
 
     parent = do_QueryObject(globalScope);
   }
 
   nsRefPtr<File> blob = new File(parent, blobImpl);
+  return blob.forget();
+}
+
+void
+ReadBlobOrFile(JSContext* aCx,
+               JSStructuredCloneReader* aReader,
+               bool aIsMainThread,
+               JS::MutableHandle<JSObject*> aBlobOrFile)
+{
+  nsRefPtr<File> blob = ReadBlobOrFileNoWrap(aCx, aReader, aIsMainThread);
   aBlobOrFile.set(blob->WrapObject(aCx));
 }
 
+// See WriteFormData for serialization format.
+void
+ReadFormData(JSContext* aCx,
+             JSStructuredCloneReader* aReader,
+             bool aIsMainThread,
+             uint32_t aCount,
+             JS::MutableHandle<JSObject*> aFormData)
+{
+  MOZ_ASSERT(aCx);
+  MOZ_ASSERT(aReader);
+  MOZ_ASSERT(!aFormData);
+
+  nsCOMPtr<nsISupports> parent;
+  if (aIsMainThread) {
+    AssertIsOnMainThread();
+    nsCOMPtr<nsIScriptGlobalObject> scriptGlobal =
+      nsJSUtils::GetStaticScriptGlobal(JS::CurrentGlobalOrNull(aCx));
+    parent = do_QueryInterface(scriptGlobal);
+  } else {
+    WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
+    MOZ_ASSERT(workerPrivate);
+    workerPrivate->AssertIsOnWorkerThread();
+
+    WorkerGlobalScope* globalScope = workerPrivate->GlobalScope();
+    MOZ_ASSERT(globalScope);
+
+    parent = do_QueryObject(globalScope);
+  }
+
+  nsRefPtr<nsFormData> formData = new nsFormData(parent);
+  MOZ_ASSERT(formData);
+
+  Optional<nsAString> thirdArg;
+
+  uint32_t isFile;
+  uint32_t dummy;
+  for (uint32_t i = 0; i < aCount; ++i) {
+    MOZ_ALWAYS_TRUE(JS_ReadUint32Pair(aReader, &isFile, &dummy));
+
+    nsAutoString name;
+    MOZ_ALWAYS_TRUE(ReadString(aReader, name));
+
+    if (isFile) {
+      // Read out the tag since the blob reader isn't expecting it.
+      MOZ_ALWAYS_TRUE(JS_ReadUint32Pair(aReader, &dummy, &dummy));
+      nsRefPtr<File> blob = ReadBlobOrFileNoWrap(aCx, aReader, aIsMainThread);
+      MOZ_ASSERT(blob);
+      formData->Append(name, *blob, thirdArg);
+    } else {
+      nsAutoString value;
+      MOZ_ALWAYS_TRUE(ReadString(aReader, value));
+      formData->Append(name, value);
+    }
+  }
+
+  aFormData.set(formData->WrapObject(aCx));
+}
+
 bool
 WriteBlobOrFile(JSContext* aCx,
                 JSStructuredCloneWriter* aWriter,
                 FileImpl* aBlobOrFileImpl,
                 nsTArray<nsCOMPtr<nsISupports>>& aClonedObjects)
 {
   MOZ_ASSERT(aCx);
   MOZ_ASSERT(aWriter);
@@ -457,16 +525,82 @@ WriteBlobOrFile(JSContext* aCx,
                                 sizeof(aBlobOrFileImpl)))) {
     return false;
   }
 
   aClonedObjects.AppendElement(aBlobOrFileImpl);
   return true;
 }
 
+// A FormData is serialized as:
+//  - A pair of ints (tag identifying it as a FormData, number of elements in
+//  the FormData)
+//  - for each (key, value) pair:
+//    - pair of ints (is value a file?, 0). If not a file, value is a string.
+//    - string name
+//    - if value is a file:
+//      - write the file/blob
+//    - else:
+//      - string value
+bool
+WriteFormData(JSContext* aCx,
+              JSStructuredCloneWriter* aWriter,
+              nsFormData* aFormData,
+              nsTArray<nsCOMPtr<nsISupports>>& aClonedObjects)
+{
+  MOZ_ASSERT(aCx);
+  MOZ_ASSERT(aWriter);
+  MOZ_ASSERT(aFormData);
+
+  if (NS_WARN_IF(!JS_WriteUint32Pair(aWriter, DOMWORKER_SCTAG_FORMDATA, aFormData->Length()))) {
+    return false;
+  }
+
+  class MOZ_STACK_CLASS Closure {
+    JSContext* mCx;
+    JSStructuredCloneWriter* mWriter;
+    nsTArray<nsCOMPtr<nsISupports>>& mClones;
+
+  public:
+    Closure(JSContext* aCx, JSStructuredCloneWriter* aWriter,
+            nsTArray<nsCOMPtr<nsISupports>>& aClones)
+      : mCx(aCx), mWriter(aWriter), mClones(aClones)
+    { }
+
+    static bool
+    Write(const nsString& aName, bool isFile, const nsString& aValue,
+          File* aFile, void* aClosure)
+    {
+      Closure* closure = static_cast<Closure*>(aClosure);
+      if (!JS_WriteUint32Pair(closure->mWriter, /* a file? */ (uint32_t) isFile, 0)) {
+        return false;
+      }
+
+      if (!WriteString(closure->mWriter, aName)) {
+        return false;
+      }
+
+      if (isFile) {
+        if (!WriteBlobOrFile(closure->mCx, closure->mWriter, aFile->Impl(), closure->mClones)) {
+          return false;
+        }
+      } else {
+        if (!WriteString(closure->mWriter, aValue)) {
+          return false;
+        }
+      }
+
+      return true;
+    }
+  };
+
+  Closure closure(aCx, aWriter, aClonedObjects);
+  return aFormData->ForEach(Closure::Write, &closure);
+}
+
 struct WorkerStructuredCloneCallbacks
 {
   static JSObject*
   Read(JSContext* aCx, JSStructuredCloneReader* aReader, uint32_t aTag,
        uint32_t aData, void* aClosure)
   {
     JS::Rooted<JSObject*> result(aCx);
 
@@ -479,16 +613,23 @@ struct WorkerStructuredCloneCallbacks
 
       return blobOrFile;
     }
     // See if the object is an ImageData.
     else if (aTag == SCTAG_DOM_IMAGEDATA) {
       MOZ_ASSERT(!aData);
       return ReadStructuredCloneImageData(aCx, aReader);
     }
+    // See if the object is a FormData.
+    else if (aTag == DOMWORKER_SCTAG_FORMDATA) {
+      JS::Rooted<JSObject*> formData(aCx);
+      // aData is the entry count.
+      ReadFormData(aCx, aReader, /* aIsMainThread */ false,  aData, &formData);
+      return formData;
+    }
 
     Error(aCx, 0);
     return nullptr;
   }
 
   static bool
   Write(JSContext* aCx, JSStructuredCloneWriter* aWriter,
         JS::Handle<JSObject*> aObj, void* aClosure)
@@ -515,16 +656,26 @@ struct WorkerStructuredCloneCallbacks
     // See if this is an ImageData object.
     {
       ImageData* imageData = nullptr;
       if (NS_SUCCEEDED(UNWRAP_OBJECT(ImageData, aObj, imageData))) {
         return WriteStructuredCloneImageData(aCx, aWriter, imageData);
       }
     }
 
+    // See if this is a FormData object.
+    {
+      nsFormData* formData = nullptr;
+      if (NS_SUCCEEDED(UNWRAP_OBJECT(FormData, aObj, formData))) {
+        if (WriteFormData(aCx, aWriter, formData, *clonedObjects)) {
+          return true;
+        }
+      }
+    }
+
     Error(aCx, 0);
     return false;
   }
 
   static void
   Error(JSContext* aCx, uint32_t /* aErrorId */)
   {
     Throw(aCx, NS_ERROR_DOM_DATA_CLONE_ERR);
@@ -552,16 +703,23 @@ struct MainThreadWorkerStructuredCloneCa
     if (aTag == DOMWORKER_SCTAG_BLOB) {
       MOZ_ASSERT(!aData);
 
       JS::Rooted<JSObject*> blobOrFile(aCx);
       ReadBlobOrFile(aCx, aReader, /* aIsMainThread */ true, &blobOrFile);
 
       return blobOrFile;
     }
+    // See if the object is a FormData.
+    else if (aTag == DOMWORKER_SCTAG_FORMDATA) {
+      JS::Rooted<JSObject*> formData(aCx);
+      // aData is the entry count.
+      ReadFormData(aCx, aReader, /* aIsMainThread */ true,  aData, &formData);
+      return formData;
+    }
 
     JS_ClearPendingException(aCx);
     return NS_DOMReadStructuredClone(aCx, aReader, aTag, aData, nullptr);
   }
 
   static bool
   Write(JSContext* aCx, JSStructuredCloneWriter* aWriter,
         JS::Handle<JSObject*> aObj, void* aClosure)
@@ -3153,17 +3311,17 @@ WorkerPrivateParent<Derived>::PostMessag
       JS_NewArrayObject(aCx, elements);
     if (!array) {
       aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
       return;
     }
     transferable.setObject(*array);
   }
 
-  nsTArray<nsCOMPtr<nsISupports> > clonedObjects;
+  nsTArray<nsCOMPtr<nsISupports>> clonedObjects;
 
   JSAutoStructuredCloneBuffer buffer;
   if (!buffer.write(aCx, aMessage, transferable, callbacks, &clonedObjects)) {
     aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR);
     return;
   }
 
   nsRefPtr<MessageEventRunnable> runnable =
--- a/dom/workers/WorkerPrivate.h
+++ b/dom/workers/WorkerPrivate.h
@@ -1288,16 +1288,17 @@ bool
 IsCurrentThreadRunningChromeWorker();
 
 JSContext*
 GetCurrentThreadJSContext();
 
 enum WorkerStructuredDataType
 {
   DOMWORKER_SCTAG_BLOB = SCTAG_DOM_MAX,
+  DOMWORKER_SCTAG_FORMDATA = SCTAG_DOM_MAX + 1,
 
   DOMWORKER_SCTAG_END
 };
 
 const JSStructuredCloneCallbacks*
 WorkerStructuredCloneCallbacks(bool aMainRuntime);
 
 const JSStructuredCloneCallbacks*
--- a/dom/workers/XMLHttpRequest.cpp
+++ b/dom/workers/XMLHttpRequest.cpp
@@ -14,16 +14,17 @@
 
 #include "jsfriendapi.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/dom/Exceptions.h"
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/ProgressEvent.h"
 #include "nsComponentManagerUtils.h"
 #include "nsContentUtils.h"
+#include "nsFormData.h"
 #include "nsJSUtils.h"
 #include "nsThreadUtils.h"
 
 #include "RuntimeService.h"
 #include "WorkerPrivate.h"
 #include "WorkerRunnable.h"
 #include "XMLHttpRequestUpload.h"
 
@@ -2195,16 +2196,54 @@ XMLHttpRequest::Send(File& aBody, ErrorR
     aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR);
     return;
   }
 
   SendInternal(EmptyString(), Move(buffer), clonedObjects, aRv);
 }
 
 void
+XMLHttpRequest::Send(nsFormData& aBody, ErrorResult& aRv)
+{
+  mWorkerPrivate->AssertIsOnWorkerThread();
+  JSContext* cx = mWorkerPrivate->GetJSContext();
+
+  if (mCanceled) {
+    aRv.ThrowUncatchableException();
+    return;
+  }
+
+  if (!mProxy) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+
+  JS::Rooted<JS::Value> value(cx);
+  if (!GetOrCreateDOMReflector(cx, &aBody, &value)) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+
+  const JSStructuredCloneCallbacks* callbacks =
+    mWorkerPrivate->IsChromeWorker() ?
+    ChromeWorkerStructuredCloneCallbacks(false) :
+    WorkerStructuredCloneCallbacks(false);
+
+  nsTArray<nsCOMPtr<nsISupports>> clonedObjects;
+
+  JSAutoStructuredCloneBuffer buffer;
+  if (!buffer.write(cx, value, callbacks, &clonedObjects)) {
+    aRv.Throw(NS_ERROR_DOM_DATA_CLONE_ERR);
+    return;
+  }
+
+  SendInternal(EmptyString(), Move(buffer), clonedObjects, aRv);
+}
+
+void
 XMLHttpRequest::Send(const ArrayBuffer& aBody, ErrorResult& aRv)
 {
   JS::Rooted<JSObject*> obj(mWorkerPrivate->GetJSContext(), aBody.Obj());
   return Send(obj, aRv);
 }
 
 void
 XMLHttpRequest::Send(const ArrayBufferView& aBody, ErrorResult& aRv)
--- a/dom/workers/XMLHttpRequest.h
+++ b/dom/workers/XMLHttpRequest.h
@@ -168,16 +168,19 @@ public:
 
   void
   Send(JS::Handle<JSObject*> aBody, ErrorResult& aRv);
 
   void
   Send(File& aBody, ErrorResult& aRv);
 
   void
+  Send(nsFormData& aBody, ErrorResult& aRv);
+
+  void
   Send(const ArrayBuffer& aBody, ErrorResult& aRv);
 
   void
   Send(const ArrayBufferView& aBody, ErrorResult& aRv);
 
   void
   Abort(ErrorResult& aRv);
 
--- a/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
+++ b/dom/workers/test/serviceworkers/test_serviceworker_interfaces.js
@@ -110,16 +110,18 @@ var interfaceNamesInGlobalScope =
     "ExtendableEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FetchEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "File",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FileReaderSync",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "FormData",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "Headers",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBCursor",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBDatabase",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBFactory",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/workers/test/test_worker_interfaces.js
+++ b/dom/workers/test/test_worker_interfaces.js
@@ -104,16 +104,18 @@ var interfaceNamesInGlobalScope =
     "Event",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "EventTarget",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "File",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "FileReaderSync",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "FormData",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "Headers",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBCursor",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBDatabase",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "IDBFactory",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/testing/web-platform/meta/workers/semantics/interface-objects/001.worker.js.ini
+++ b/testing/web-platform/meta/workers/semantics/interface-objects/001.worker.js.ini
@@ -10,19 +10,16 @@
     expected: FAIL
 
   [The FileList interface object should be exposed.]
     expected: FAIL
 
   [The ProgressEvent interface object should be exposed.]
     expected: FAIL
 
-  [The FormData interface object should be exposed.]
-    expected: FAIL
-
   [The CanvasProxy interface object should be exposed.]
     expected: FAIL
 
   [The ImageBitmap interface object should be exposed.]
     expected: FAIL
 
   [The CanvasRenderingContext2D interface object should be exposed.]
     expected: FAIL