Bug 1073231 Implement Request and Response Clone() methods. r=nsm r=baku
authorBen Kelly <ben@wanderview.com>
Thu, 19 Feb 2015 20:24:24 -0500
changeset 229952 39c5fd86a4a0bd76810e3d481b36b82f92da17bf
parent 229951 fb6b682a464e0bd9f30fa2004c73d907f34a9a65
child 229953 0c200f94c3cf9d1c51f844b3cfeefb8fe0cde5a9
push id55850
push userbkelly@mozilla.com
push dateFri, 20 Feb 2015 01:24:32 +0000
treeherdermozilla-inbound@39c5fd86a4a0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnsm, baku
bugs1073231
milestone38.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 1073231 Implement Request and Response Clone() methods. r=nsm r=baku
dom/bindings/Errors.msg
dom/fetch/Fetch.cpp
dom/fetch/Fetch.h
dom/fetch/InternalRequest.cpp
dom/fetch/InternalRequest.h
dom/fetch/InternalResponse.cpp
dom/fetch/InternalResponse.h
dom/fetch/Request.cpp
dom/fetch/Request.h
dom/fetch/Response.cpp
dom/fetch/Response.h
dom/webidl/Request.webidl
dom/webidl/Response.webidl
dom/workers/test/fetch/worker_test_request.js
dom/workers/test/fetch/worker_test_response.js
--- a/dom/bindings/Errors.msg
+++ b/dom/bindings/Errors.msg
@@ -57,17 +57,17 @@ MSG_DEF(MSG_METADATA_NOT_CONFIGURED, 0, 
 MSG_DEF(MSG_INVALID_READ_SIZE, 0, JSEXN_TYPEERR, "0 (Zero) is not a valid read size.")
 MSG_DEF(MSG_HEADERS_IMMUTABLE, 0, JSEXN_TYPEERR, "Headers are immutable and cannot be modified.")
 MSG_DEF(MSG_INVALID_HEADER_NAME, 1, JSEXN_TYPEERR, "{0} is an invalid header name.")
 MSG_DEF(MSG_INVALID_HEADER_VALUE, 1, JSEXN_TYPEERR, "{0} is an invalid header value.")
 MSG_DEF(MSG_INVALID_HEADER_SEQUENCE, 0, JSEXN_TYPEERR, "Headers require name/value tuples when being initialized by a sequence.")
 MSG_DEF(MSG_PERMISSION_DENIED_TO_PASS_ARG, 1, JSEXN_TYPEERR, "Permission denied to pass cross-origin object as {0}.")
 MSG_DEF(MSG_MISSING_REQUIRED_DICTIONARY_MEMBER, 1, JSEXN_TYPEERR, "Missing required {0}.")
 MSG_DEF(MSG_INVALID_REQUEST_METHOD, 1, JSEXN_TYPEERR, "Invalid request method {0}.")
-MSG_DEF(MSG_REQUEST_BODY_CONSUMED_ERROR, 0, JSEXN_TYPEERR, "Request body has already been consumed.")
+MSG_DEF(MSG_FETCH_BODY_CONSUMED_ERROR, 0, JSEXN_TYPEERR, "Body has already been consumed.")
 MSG_DEF(MSG_RESPONSE_INVALID_STATUSTEXT_ERROR, 0, JSEXN_TYPEERR, "Response statusText may not contain newline or carriage return.")
 MSG_DEF(MSG_FETCH_FAILED, 0, JSEXN_TYPEERR, "NetworkError when attempting to fetch resource.")
 MSG_DEF(MSG_NO_BODY_ALLOWED_FOR_GET_AND_HEAD, 0, JSEXN_TYPEERR, "HEAD or GET Request cannot have a body.")
 MSG_DEF(MSG_DEFINE_NON_CONFIGURABLE_PROP_ON_WINDOW, 0, JSEXN_TYPEERR, "Not allowed to define a non-configurable property on the WindowProxy object")
 MSG_DEF(MSG_INVALID_ZOOMANDPAN_VALUE_ERROR, 0, JSEXN_RANGEERR, "Invalid zoom and pan value.")
 MSG_DEF(MSG_INVALID_TRANSFORM_ANGLE_ERROR, 0, JSEXN_RANGEERR, "Invalid transform angle.")
 MSG_DEF(MSG_INVALID_RESPONSE_STATUSCODE_ERROR, 0, JSEXN_RANGEERR, "Invalid response status code.")
 MSG_DEF(MSG_INVALID_REDIRECT_STATUSCODE_ERROR, 0, JSEXN_RANGEERR, "Invalid redirect status code.")
--- a/dom/fetch/Fetch.cpp
+++ b/dom/fetch/Fetch.cpp
@@ -1159,17 +1159,17 @@ FetchBody<Derived>::ContinueConsumeBody(
 }
 
 template <class Derived>
 already_AddRefed<Promise>
 FetchBody<Derived>::ConsumeBody(ConsumeType aType, ErrorResult& aRv)
 {
   mConsumeType = aType;
   if (BodyUsed()) {
-    aRv.ThrowTypeError(MSG_REQUEST_BODY_CONSUMED_ERROR);
+    aRv.ThrowTypeError(MSG_FETCH_BODY_CONSUMED_ERROR);
     return nullptr;
   }
 
   SetBodyUsed();
 
   mConsumePromise = Promise::Create(DerivedClass()->GetParentObject(), aRv);
   if (aRv.Failed()) {
     return nullptr;
--- a/dom/fetch/Fetch.h
+++ b/dom/fetch/Fetch.h
@@ -94,17 +94,17 @@ template <class Derived> class FetchBody
  *    worry about keeping anything alive.
  *
  * The pump is always released on the main thread.
  */
 template <class Derived>
 class FetchBody {
 public:
   bool
-  BodyUsed() { return mBodyUsed; }
+  BodyUsed() const { return mBodyUsed; }
 
   already_AddRefed<Promise>
   ArrayBuffer(ErrorResult& aRv)
   {
     return ConsumeBody(CONSUME_ARRAYBUFFER, aRv);
   }
 
   already_AddRefed<Promise>
--- a/dom/fetch/InternalRequest.cpp
+++ b/dom/fetch/InternalRequest.cpp
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "InternalRequest.h"
 
 #include "nsIContentPolicy.h"
 #include "nsIDocument.h"
+#include "nsStreamUtils.h"
 
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/ScriptSettings.h"
 #include "mozilla/dom/workers/Workers.h"
 
 #include "WorkerPrivate.h"
 
 namespace mozilla {
@@ -38,14 +39,61 @@ InternalRequest::GetRequestConstructorCo
 
   copy->mContentPolicyType = nsIContentPolicy::TYPE_FETCH;
   copy->mMode = mMode;
   copy->mCredentialsMode = mCredentialsMode;
   copy->mCacheMode = mCacheMode;
   return copy.forget();
 }
 
+already_AddRefed<InternalRequest>
+InternalRequest::Clone()
+{
+  nsRefPtr<InternalRequest> clone = new InternalRequest(*this);
+
+  if (!mBodyStream) {
+    return clone.forget();
+  }
+
+  nsCOMPtr<nsIInputStream> clonedBody;
+  nsCOMPtr<nsIInputStream> replacementBody;
+
+  nsresult rv = NS_CloneInputStream(mBodyStream, getter_AddRefs(clonedBody),
+                                    getter_AddRefs(replacementBody));
+  if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; }
+
+  clone->mBodyStream.swap(clonedBody);
+  if (replacementBody) {
+    mBodyStream.swap(replacementBody);
+  }
+
+  return clone.forget();
+}
+
+InternalRequest::InternalRequest(const InternalRequest& aOther)
+  : mMethod(aOther.mMethod)
+  , mURL(aOther.mURL)
+  , mHeaders(new InternalHeaders(*aOther.mHeaders))
+  , mContentPolicyType(aOther.mContentPolicyType)
+  , mReferrer(aOther.mReferrer)
+  , mMode(aOther.mMode)
+  , mCredentialsMode(aOther.mCredentialsMode)
+  , mResponseTainting(aOther.mResponseTainting)
+  , mCacheMode(aOther.mCacheMode)
+  , mAuthenticationFlag(aOther.mAuthenticationFlag)
+  , mForceOriginHeader(aOther.mForceOriginHeader)
+  , mPreserveContentCodings(aOther.mPreserveContentCodings)
+  , mSameOriginDataURL(aOther.mSameOriginDataURL)
+  , mSandboxedStorageAreaURLs(aOther.mSandboxedStorageAreaURLs)
+  , mSkipServiceWorker(aOther.mSkipServiceWorker)
+  , mSynchronous(aOther.mSynchronous)
+  , mUnsafeRequest(aOther.mUnsafeRequest)
+  , mUseURLCredentials(aOther.mUseURLCredentials)
+{
+  // NOTE: does not copy body stream... use the fallible Clone() for that
+}
+
 InternalRequest::~InternalRequest()
 {
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/fetch/InternalRequest.h
+++ b/dom/fetch/InternalRequest.h
@@ -62,38 +62,17 @@ public:
     , mSameOriginDataURL(true)
     , mSkipServiceWorker(false)
     , mSynchronous(false)
     , mUnsafeRequest(false)
     , mUseURLCredentials(false)
   {
   }
 
-  explicit InternalRequest(const InternalRequest& aOther)
-    : mMethod(aOther.mMethod)
-    , mURL(aOther.mURL)
-    , mHeaders(aOther.mHeaders)
-    , mBodyStream(aOther.mBodyStream)
-    , mContentPolicyType(aOther.mContentPolicyType)
-    , mReferrer(aOther.mReferrer)
-    , mMode(aOther.mMode)
-    , mCredentialsMode(aOther.mCredentialsMode)
-    , mResponseTainting(aOther.mResponseTainting)
-    , mCacheMode(aOther.mCacheMode)
-    , mAuthenticationFlag(aOther.mAuthenticationFlag)
-    , mForceOriginHeader(aOther.mForceOriginHeader)
-    , mPreserveContentCodings(aOther.mPreserveContentCodings)
-    , mSameOriginDataURL(aOther.mSameOriginDataURL)
-    , mSandboxedStorageAreaURLs(aOther.mSandboxedStorageAreaURLs)
-    , mSkipServiceWorker(aOther.mSkipServiceWorker)
-    , mSynchronous(aOther.mSynchronous)
-    , mUnsafeRequest(aOther.mUnsafeRequest)
-    , mUseURLCredentials(aOther.mUseURLCredentials)
-  {
-  }
+  already_AddRefed<InternalRequest> Clone();
 
   void
   GetMethod(nsCString& aMethod) const
   {
     aMethod.Assign(mMethod);
   }
 
   void
@@ -288,16 +267,19 @@ public:
     s.forget(aStream);
   }
 
   // The global is used as the client for the new object.
   already_AddRefed<InternalRequest>
   GetRequestConstructorCopy(nsIGlobalObject* aGlobal, ErrorResult& aRv) const;
 
 private:
+  // Does not copy mBodyStream.  Use fallible Clone() for complete copy.
+  explicit InternalRequest(const InternalRequest& aOther);
+
   ~InternalRequest();
 
   nsCString mMethod;
   nsCString mURL;
   nsRefPtr<InternalHeaders> mHeaders;
   nsCOMPtr<nsIInputStream> mBodyStream;
 
   // nsContentPolicyType does not cover the complete set defined in the spec,
--- a/dom/fetch/InternalResponse.cpp
+++ b/dom/fetch/InternalResponse.cpp
@@ -3,59 +3,86 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "InternalResponse.h"
 
 #include "nsIDOMFile.h"
 
 #include "mozilla/dom/InternalHeaders.h"
+#include "nsStreamUtils.h"
 
 namespace mozilla {
 namespace dom {
 
 InternalResponse::InternalResponse(uint16_t aStatus, const nsACString& aStatusText)
   : mType(ResponseType::Default)
   , mFinalURL(false)
   , mStatus(aStatus)
   , mStatusText(aStatusText)
   , mHeaders(new InternalHeaders(HeadersGuardEnum::Response))
 {
 }
 
 // Headers are not copied since BasicResponse and CORSResponse both need custom
-// header handling.
+// header handling.  Body is not copied as it cannot be shared directly.
 InternalResponse::InternalResponse(const InternalResponse& aOther)
   : mType(aOther.mType)
   , mTerminationReason(aOther.mTerminationReason)
   , mURL(aOther.mURL)
   , mFinalURL(aOther.mFinalURL)
   , mStatus(aOther.mStatus)
   , mStatusText(aOther.mStatusText)
-  , mBody(aOther.mBody)
   , mContentType(aOther.mContentType)
 {
 }
 
+already_AddRefed<InternalResponse>
+InternalResponse::Clone()
+{
+  nsRefPtr<InternalResponse> clone = new InternalResponse(*this);
+  clone->mHeaders = new InternalHeaders(*mHeaders);
+
+  if (!mBody) {
+    return clone.forget();
+  }
+
+  nsCOMPtr<nsIInputStream> clonedBody;
+  nsCOMPtr<nsIInputStream> replacementBody;
+
+  nsresult rv = NS_CloneInputStream(mBody, getter_AddRefs(clonedBody),
+                                    getter_AddRefs(replacementBody));
+  if (NS_WARN_IF(NS_FAILED(rv))) { return nullptr; }
+
+  clone->mBody.swap(clonedBody);
+  if (replacementBody) {
+    mBody.swap(replacementBody);
+  }
+
+  return clone.forget();
+}
+
 // static
 already_AddRefed<InternalResponse>
 InternalResponse::BasicResponse(InternalResponse* aInner)
 {
   MOZ_ASSERT(aInner);
   nsRefPtr<InternalResponse> basic = new InternalResponse(*aInner);
   basic->mType = ResponseType::Basic;
   basic->mHeaders = InternalHeaders::BasicHeaders(aInner->mHeaders);
+  basic->mBody.swap(aInner->mBody);
   return basic.forget();
 }
 
 // static
 already_AddRefed<InternalResponse>
 InternalResponse::CORSResponse(InternalResponse* aInner)
 {
   MOZ_ASSERT(aInner);
   nsRefPtr<InternalResponse> cors = new InternalResponse(*aInner);
   cors->mType = ResponseType::Cors;
   cors->mHeaders = InternalHeaders::CORSHeaders(aInner->mHeaders);
+  cors->mBody.swap(aInner->mBody);
   return cors.forget();
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/fetch/InternalResponse.h
+++ b/dom/fetch/InternalResponse.h
@@ -20,16 +20,18 @@ class InternalResponse MOZ_FINAL
 {
   friend class FetchDriver;
 
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InternalResponse)
 
   InternalResponse(uint16_t aStatus, const nsACString& aStatusText);
 
+  already_AddRefed<InternalResponse> Clone();
+
   static already_AddRefed<InternalResponse>
   NetworkError()
   {
     nsRefPtr<InternalResponse> response = new InternalResponse(0, EmptyCString());
     response->mType = ResponseType::Error;
     return response.forget();
   }
 
@@ -120,18 +122,18 @@ public:
     MOZ_ASSERT(!mBody);
     mBody = aBody;
   }
 
 private:
   ~InternalResponse()
   { }
 
-  // Used to create filtered responses.
-  // Does not copy headers.
+  // Used to create filtered and cloned responses.
+  // Does not copy headers or body stream.
   explicit InternalResponse(const InternalResponse& aOther);
 
   ResponseType mType;
   nsCString mTerminationReason;
   nsCString mURL;
   bool mFinalURL;
   const uint16_t mStatus;
   const nsCString mStatusText;
--- a/dom/fetch/Request.cpp
+++ b/dom/fetch/Request.cpp
@@ -58,17 +58,17 @@ Request::Constructor(const GlobalObject&
   nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
 
   if (aInput.IsRequest()) {
     nsRefPtr<Request> inputReq = &aInput.GetAsRequest();
     nsCOMPtr<nsIInputStream> body;
     inputReq->GetBody(getter_AddRefs(body));
     if (body) {
       if (inputReq->BodyUsed()) {
-        aRv.ThrowTypeError(MSG_REQUEST_BODY_CONSUMED_ERROR);
+        aRv.ThrowTypeError(MSG_FETCH_BODY_CONSUMED_ERROR);
         return nullptr;
       } else {
         inputReq->SetBodyUsed();
       }
     }
 
     request = inputReq->GetInternalRequest();
   } else {
@@ -250,22 +250,30 @@ Request::Constructor(const GlobalObject&
   }
 
   nsRefPtr<Request> domRequest = new Request(global, request);
   domRequest->SetMimeType(aRv);
   return domRequest.forget();
 }
 
 already_AddRefed<Request>
-Request::Clone() const
+Request::Clone(ErrorResult& aRv) const
 {
-  // FIXME(nsm): Bug 1073231. This is incorrect, but the clone method isn't
-  // well defined yet.
-  nsRefPtr<Request> request = new Request(mOwner,
-                                          new InternalRequest(*mRequest));
+  if (BodyUsed()) {
+    aRv.ThrowTypeError(MSG_FETCH_BODY_CONSUMED_ERROR);
+    return nullptr;
+  }
+
+  nsRefPtr<InternalRequest> ir = mRequest->Clone();
+  if (!ir) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return nullptr;
+  }
+
+  nsRefPtr<Request> request = new Request(mOwner, ir);
   return request.forget();
 }
 
 Headers*
 Request::Headers_()
 {
   if (!mHeaders) {
     mHeaders = new Headers(mOwner, mRequest->Headers());
--- a/dom/fetch/Request.h
+++ b/dom/fetch/Request.h
@@ -110,17 +110,17 @@ public:
               const RequestInit& aInit, ErrorResult& rv);
 
   nsIGlobalObject* GetParentObject() const
   {
     return mOwner;
   }
 
   already_AddRefed<Request>
-  Clone() const;
+  Clone(ErrorResult& aRv) const;
 
   already_AddRefed<InternalRequest>
   GetInternalRequest();
 private:
   ~Request();
 
   nsCOMPtr<nsIGlobalObject> mOwner;
   nsRefPtr<InternalRequest> mRequest;
--- a/dom/fetch/Response.cpp
+++ b/dom/fetch/Response.cpp
@@ -185,29 +185,33 @@ Response::Constructor(const GlobalObject
       return nullptr;
     }
   }
 
   r->SetMimeType(aRv);
   return r.forget();
 }
 
-// FIXME(nsm): Bug 1073231: This is currently unspecced!
 already_AddRefed<Response>
-Response::Clone()
+Response::Clone(ErrorResult& aRv) const
 {
-  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mOwner);
-  nsRefPtr<Response> response = new Response(global, mInternalResponse);
+  if (BodyUsed()) {
+    aRv.ThrowTypeError(MSG_FETCH_BODY_CONSUMED_ERROR);
+    return nullptr;
+  }
+
+  nsRefPtr<InternalResponse> ir = mInternalResponse->Clone();
+  nsRefPtr<Response> response = new Response(mOwner, ir);
   return response.forget();
 }
 
 void
 Response::SetBody(nsIInputStream* aBody)
 {
-  // FIXME(nsm): Do we flip bodyUsed here?
+  MOZ_ASSERT(!BodyUsed());
   mInternalResponse->SetBody(aBody);
 }
 
 Headers*
 Response::Headers_()
 {
   if (!mHeaders) {
     mHeaders = new Headers(mOwner, mInternalResponse->Headers());
--- a/dom/fetch/Response.h
+++ b/dom/fetch/Response.h
@@ -107,17 +107,17 @@ public:
               const ResponseInit& aInit, ErrorResult& rv);
 
   nsIGlobalObject* GetParentObject() const
   {
     return mOwner;
   }
 
   already_AddRefed<Response>
-  Clone();
+  Clone(ErrorResult& aRv) const;
 
   void
   SetBody(nsIInputStream* aBody);
 private:
   ~Response();
 
   nsCOMPtr<nsIGlobalObject> mOwner;
   nsRefPtr<InternalResponse> mInternalResponse;
--- a/dom/webidl/Request.webidl
+++ b/dom/webidl/Request.webidl
@@ -18,17 +18,18 @@ interface Request {
   [SameObject] readonly attribute Headers headers;
 
   readonly attribute RequestContext context;
   readonly attribute DOMString referrer;
   readonly attribute RequestMode mode;
   readonly attribute RequestCredentials credentials;
   readonly attribute RequestCache cache;
 
-  [NewObject] Request clone();
+  [Throws,
+   NewObject] Request clone();
 
   // Bug 1124638 - Allow chrome callers to set the context.
   [ChromeOnly]
   void setContext(RequestContext context);
 };
 Request implements Body;
 
 dictionary RequestInit {
--- a/dom/webidl/Response.webidl
+++ b/dom/webidl/Response.webidl
@@ -20,17 +20,18 @@ interface Response {
   readonly attribute USVString url;
   [Throws]
            attribute boolean finalURL;
   readonly attribute unsigned short status;
   readonly attribute boolean ok;
   readonly attribute ByteString statusText;
   [SameObject] readonly attribute Headers headers;
 
-  [NewObject] Response clone();
+  [Throws,
+   NewObject] Response clone();
 };
 Response implements Body;
 
 dictionary ResponseInit {
   unsigned short status = 200;
   // WebIDL spec doesn't allow default values for ByteString.
   ByteString statusText;
   HeadersInit headers;
--- a/dom/workers/test/fetch/worker_test_request.js
+++ b/dom/workers/test/fetch/worker_test_request.js
@@ -22,31 +22,80 @@ function testDefaultCtor() {
   is(req.url, self.location.href, "URL should be resolved with entry settings object's API base URL");
   is(req.context, "fetch", "Default context is fetch.");
   is(req.referrer, "about:client", "Default referrer is `client` which serializes to about:client.");
   is(req.mode, "cors", "Request mode string input is cors");
   is(req.credentials, "omit", "Default Request credentials is omit");
 }
 
 function testClone() {
-  var req = (new Request("./cloned_request.txt", {
+  var orig = new Request("./cloned_request.txt", {
               method: 'POST',
               headers: { "Content-Length": 5 },
               body: "Sample body",
               mode: "same-origin",
               credentials: "same-origin",
-            })).clone();
-  ok(req.method === "POST", "Request method is POST");
-  ok(req.headers instanceof Headers, "Request should have non-null Headers object");
-  is(req.headers.get('content-length'), "5", "Request content-length should be 5.");
-  ok(req.url === (new URL("./cloned_request.txt", self.location.href)).href,
+            });
+  var clone = orig.clone();
+  ok(clone.method === "POST", "Request method is POST");
+  ok(clone.headers instanceof Headers, "Request should have non-null Headers object");
+
+  is(clone.headers.get('content-length'), "5", "Response content-length should be 5.");
+  orig.headers.set('content-length', 6);
+  is(clone.headers.get('content-length'), "5", "Request content-length should be 5.");
+
+  ok(clone.url === (new URL("./cloned_request.txt", self.location.href)).href,
        "URL should be resolved with entry settings object's API base URL");
-  ok(req.referrer === "about:client", "Default referrer is `client` which serializes to about:client.");
-  ok(req.mode === "same-origin", "Request mode is same-origin");
-  ok(req.credentials === "same-origin", "Default credentials is same-origin");
+  ok(clone.referrer === "about:client", "Default referrer is `client` which serializes to about:client.");
+  ok(clone.mode === "same-origin", "Request mode is same-origin");
+  ok(clone.credentials === "same-origin", "Default credentials is same-origin");
+
+  ok(!orig.bodyUsed, "Original body is not consumed.");
+  ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+  var origBody = null;
+  var clone2 = null;
+  return orig.text().then(function (body) {
+    origBody = body;
+    is(origBody, "Sample body", "Original body string matches");
+    ok(orig.bodyUsed, "Original body is consumed.");
+    ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+    try {
+      orig.clone()
+      ok(false, "Cannot clone Request whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    clone2 = clone.clone();
+    return clone.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone.clone()
+      ok(false, "Cannot clone Request whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    return clone2.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone2.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone2.clone()
+      ok(false, "Cannot clone Request whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+  });
 }
 
 function testUsedRequest() {
   // Passing a used request should fail.
   var req = new Request("", { method: 'post', body: "This is foo" });
   var p1 = req.text().then(function(v) {
     try {
       var req2 = new Request(req);
@@ -261,27 +310,27 @@ function testModeCorsPreflightEnumValue(
        "mode cors-with-forced-preflight should throw same error as invalid RequestMode strings.");
   }
 }
 
 onmessage = function() {
   var done = function() { postMessage({ type: 'finish' }) }
 
   testDefaultCtor();
-  testClone();
   testSimpleUrlParse();
   testUrlFragment();
   testMethod();
   testBug1109574();
   testModeCorsPreflightEnumValue();
 
   Promise.resolve()
     .then(testBodyCreation)
     .then(testBodyUsed)
     .then(testBodyExtraction)
     .then(testUsedRequest)
+    .then(testClone())
     // Put more promise based tests here.
     .then(done)
     .catch(function(e) {
       ok(false, "Some Request tests failed " + e);
       done();
     })
 }
--- a/dom/workers/test/fetch/worker_test_response.js
+++ b/dom/workers/test/fetch/worker_test_response.js
@@ -13,25 +13,73 @@ function testDefaultCtor() {
   is(res.type, "default", "Default Response type is default");
   ok(res.headers instanceof Headers, "Response should have non-null Headers object");
   is(res.url, "", "URL should be empty string");
   is(res.status, 200, "Default status is 200");
   is(res.statusText, "OK", "Default statusText is OK");
 }
 
 function testClone() {
-  var res = (new Response("This is a body", {
+  var orig = new Response("This is a body", {
               status: 404,
               statusText: "Not Found",
               headers: { "Content-Length": 5 },
-            })).clone();
-  is(res.status, 404, "Response status is 404");
-  is(res.statusText, "Not Found", "Response statusText is POST");
-  ok(res.headers instanceof Headers, "Response should have non-null Headers object");
-  is(res.headers.get('content-length'), "5", "Response content-length should be 5.");
+            });
+  var clone = orig.clone();
+  is(clone.status, 404, "Response status is 404");
+  is(clone.statusText, "Not Found", "Response statusText is POST");
+  ok(clone.headers instanceof Headers, "Response should have non-null Headers object");
+
+  is(clone.headers.get('content-length'), "5", "Response content-length should be 5.");
+  orig.headers.set('content-length', 6);
+  is(clone.headers.get('content-length'), "5", "Response content-length should be 5.");
+
+  ok(!orig.bodyUsed, "Original body is not consumed.");
+  ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+  var origBody = null;
+  var clone2 = null;
+  return orig.text().then(function (body) {
+    origBody = body;
+    is(origBody, "This is a body", "Original body string matches");
+    ok(orig.bodyUsed, "Original body is consumed.");
+    ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+    try {
+      orig.clone()
+      ok(false, "Cannot clone Response whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    clone2 = clone.clone();
+    return clone.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone.clone()
+      ok(false, "Cannot clone Response whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    return clone2.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone2.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone2.clone()
+      ok(false, "Cannot clone Response whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+  });
 }
 
 function testRedirect() {
   var res = Response.redirect("./redirect.response");
   is(res.status, 302, "Default redirect has status code 302");
   var h = res.headers.get("location");
   ok(h === (new URL("./redirect.response", self.location.href)).href, "Location header should be correct absolute URL");