From cb61a019fcead3e264d61010f5fe7407a87c9f8f Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Wed, 18 Feb 2015 14:10:52 -0500 Subject: [PATCH] Bug 1065216 - Dispatch a fetch event to workers when controlled pages initiate a network load. r=baku,mayhemer,smaug --- docshell/base/nsDocShell.cpp | 48 +++ docshell/base/nsDocShell.h | 3 + dom/base/nsDocument.h | 2 +- dom/bindings/Bindings.conf | 8 + dom/fetch/Fetch.cpp | 5 + dom/fetch/Fetch.h | 16 +- .../base/nsIServiceWorkerManager.idl | 12 +- dom/webidl/FetchEvent.webidl | 27 ++ dom/webidl/moz.build | 1 + dom/workers/ServiceWorkerEvents.cpp | 276 +++++++++++++++++ dom/workers/ServiceWorkerEvents.h | 75 +++++ dom/workers/ServiceWorkerManager.cpp | 287 +++++++++++++++++- dom/workers/ServiceWorkerManager.h | 3 + .../test/serviceworkers/fetch/fetch_tests.js | 54 ++++ .../fetch/fetch_worker_script.js | 29 ++ .../test/serviceworkers/fetch/index.html | 147 +++++++++ .../test/serviceworkers/fetch_event_worker.js | 86 ++++++ dom/workers/test/serviceworkers/mochitest.ini | 5 + .../test/serviceworkers/test_fetch_event.html | 61 ++++ .../base/nsINetworkInterceptController.idl | 34 ++- netwerk/protocol/http/HttpBaseChannel.cpp | 10 +- netwerk/protocol/http/HttpBaseChannel.h | 2 +- netwerk/protocol/http/HttpChannelChild.cpp | 203 ++++++++----- netwerk/protocol/http/HttpChannelChild.h | 2 +- netwerk/protocol/http/InterceptedChannel.cpp | 98 ++++-- netwerk/protocol/http/InterceptedChannel.h | 31 +- .../test/unit/test_synthesized_response.js | 73 ++++- 27 files changed, 1450 insertions(+), 148 deletions(-) create mode 100644 dom/webidl/FetchEvent.webidl create mode 100644 dom/workers/test/serviceworkers/fetch/fetch_tests.js create mode 100644 dom/workers/test/serviceworkers/fetch/fetch_worker_script.js create mode 100644 dom/workers/test/serviceworkers/fetch/index.html create mode 100644 dom/workers/test/serviceworkers/fetch_event_worker.js create mode 100644 dom/workers/test/serviceworkers/test_fetch_event.html diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index d6f2faf92e04..48e9612f457b 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -52,6 +52,7 @@ #include "nsIAuthPrompt2.h" #include "nsIChannelEventSink.h" #include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIServiceWorkerManager.h" #include "nsIScriptSecurityManager.h" #include "nsIScriptObjectPrincipal.h" #include "nsIScrollableFrame.h" @@ -1041,6 +1042,7 @@ NS_INTERFACE_MAP_BEGIN(nsDocShell) NS_INTERFACE_MAP_ENTRY(nsILinkHandler) NS_INTERFACE_MAP_ENTRY(nsIClipboardCommands) NS_INTERFACE_MAP_ENTRY(nsIDOMStorageManager) + NS_INTERFACE_MAP_ENTRY(nsINetworkInterceptController) NS_INTERFACE_MAP_END_INHERITING(nsDocLoader) ///***************************************************************************** @@ -13921,6 +13923,52 @@ nsDocShell::MaybeNotifyKeywordSearchLoading(const nsString& aProvider, #endif } +NS_IMETHODIMP +nsDocShell::ShouldPrepareForIntercept(nsIURI* aURI, bool aIsNavigate, bool* aShouldIntercept) +{ + *aShouldIntercept = false; + nsCOMPtr swm = mozilla::services::GetServiceWorkerManager(); + if (!swm) { + return NS_OK; + } + + if (aIsNavigate) { + return swm->IsAvailableForURI(aURI, aShouldIntercept); + } + + nsCOMPtr doc = GetDocument(); + if (!doc) { + return NS_ERROR_NOT_AVAILABLE; + } + + return swm->IsControlled(doc, aShouldIntercept); +} + +NS_IMETHODIMP +nsDocShell::ChannelIntercepted(nsIInterceptedChannel* aChannel) +{ + nsCOMPtr swm = mozilla::services::GetServiceWorkerManager(); + if (!swm) { + aChannel->Cancel(); + return NS_OK; + } + + bool isNavigation = false; + nsresult rv = aChannel->GetIsNavigation(&isNavigation); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr doc; + + if (!isNavigation) { + doc = GetDocument(); + if (!doc) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + return swm->DispatchFetchEvent(doc, aChannel); +} + NS_IMETHODIMP nsDocShell::SetPaymentRequestId(const nsAString& aPaymentRequestId) { diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index d1a8ad2088a1..9b5094f2715f 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -12,6 +12,7 @@ #include "nsIDocShell.h" #include "nsIDocShellTreeItem.h" #include "nsIBaseWindow.h" +#include "nsINetworkInterceptController.h" #include "nsIScrollable.h" #include "nsITextScroll.h" #include "nsIContentViewerContainer.h" @@ -152,6 +153,7 @@ class nsDocShell MOZ_FINAL , public nsILinkHandler , public nsIClipboardCommands , public nsIDOMStorageManager + , public nsINetworkInterceptController , public mozilla::SupportsWeakPtr { friend class nsDSURIContentListener; @@ -182,6 +184,7 @@ public: NS_DECL_NSIAUTHPROMPTPROVIDER NS_DECL_NSICLIPBOARDCOMMANDS NS_DECL_NSIWEBSHELLSERVICES + NS_DECL_NSINETWORKINTERCEPTCONTROLLER NS_FORWARD_SAFE_NSIDOMSTORAGEMANAGER(TopSessionStorageManager()) NS_IMETHOD Stop() MOZ_OVERRIDE { diff --git a/dom/base/nsDocument.h b/dom/base/nsDocument.h index 2b3269416500..3cbd872af6be 100644 --- a/dom/base/nsDocument.h +++ b/dom/base/nsDocument.h @@ -622,7 +622,7 @@ protected: DECL_SHIM(nsIApplicationCacheContainer, NSIAPPLICATIONCACHECONTAINER) #undef DECL_SHIM }; - + /** * Add an ExternalResource for aURI. aViewer and aLoadGroup might be null * when this is called if the URI didn't result in an XML document. This diff --git a/dom/bindings/Bindings.conf b/dom/bindings/Bindings.conf index 3d3390fc2ac6..6555867e4241 100644 --- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -418,6 +418,14 @@ DOMInterfaces = { 'nativeType': 'mozilla::dom::workers::ExtendableEvent', }, +'FetchEvent': { + 'headerFile': 'ServiceWorkerEvents.h', + 'nativeType': 'mozilla::dom::workers::FetchEvent', + 'binaryNames': { + 'request': 'request_' + }, +}, + 'FileList': { 'headerFile': 'mozilla/dom/File.h', }, diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp index 9abbd7824c3e..ce1e7c822c55 100644 --- a/dom/fetch/Fetch.cpp +++ b/dom/fetch/Fetch.cpp @@ -866,6 +866,11 @@ FetchBody::FetchBody(); template FetchBody::FetchBody(); +template +FetchBody::~FetchBody() +{ +} + // Returns true if addref succeeded. // Always succeeds on main thread. // May fail on worker if RegisterFeature() fails. In that case, it will release diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h index d3090fea1a5e..6b86f0d444af 100644 --- a/dom/fetch/Fetch.h +++ b/dom/fetch/Fetch.h @@ -135,6 +135,12 @@ public: void CancelPump(); + void + SetBodyUsed() + { + mBodyUsed = true; + } + // Always set whenever the FetchBody is created on the worker thread. workers::WorkerPrivate* mWorkerPrivate; @@ -145,15 +151,7 @@ public: protected: FetchBody(); - virtual ~FetchBody() - { - } - - void - SetBodyUsed() - { - mBodyUsed = true; - } + virtual ~FetchBody(); void SetMimeType(ErrorResult& aRv); diff --git a/dom/interfaces/base/nsIServiceWorkerManager.idl b/dom/interfaces/base/nsIServiceWorkerManager.idl index acd45e9d5182..2cec7919a442 100644 --- a/dom/interfaces/base/nsIServiceWorkerManager.idl +++ b/dom/interfaces/base/nsIServiceWorkerManager.idl @@ -6,6 +6,7 @@ #include "domstubs.idl" interface nsIDocument; +interface nsIInterceptedChannel; interface nsIPrincipal; interface nsIURI; @@ -18,7 +19,7 @@ interface nsIServiceWorkerUnregisterCallback : nsISupports [noscript] void UnregisterFailed(); }; -[builtinclass, uuid(861b55e9-d6ac-47cf-a528-8590e9b44de6)] +[builtinclass, uuid(464882c8-81c0-4620-b9c4-44c12085b65b)] interface nsIServiceWorkerManager : nsISupports { /** @@ -51,6 +52,15 @@ interface nsIServiceWorkerManager : nsISupports // Remove ready pending Promise void removeReadyPromise(in nsIDOMWindow aWindow); + // Returns true if a ServiceWorker is available for the scope of aURI. + bool isAvailableForURI(in nsIURI aURI); + + // Returns true if a given document is currently controlled by a ServiceWorker + bool isControlled(in nsIDocument aDocument); + + // Cause a fetch event to be dispatched to the worker global associated with the given document. + void dispatchFetchEvent(in nsIDocument aDoc, in nsIInterceptedChannel aChannel); + // aTarget MUST be a ServiceWorkerRegistration. [noscript] void AddRegistrationEventListener(in DOMString aScope, in nsIDOMEventTarget aTarget); [noscript] void RemoveRegistrationEventListener(in DOMString aScope, in nsIDOMEventTarget aTarget); diff --git a/dom/webidl/FetchEvent.webidl b/dom/webidl/FetchEvent.webidl new file mode 100644 index 000000000000..20c378176743 --- /dev/null +++ b/dom/webidl/FetchEvent.webidl @@ -0,0 +1,27 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. + * + * For more information on this interface, please see + * http://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html + */ + +[Constructor(DOMString type, optional FetchEventInit eventInitDict), + Func="mozilla::dom::workers::ServiceWorkerVisible", + Exposed=(ServiceWorker)] +interface FetchEvent : Event { + readonly attribute Request request; + readonly attribute ServiceWorkerClient client; // The window issuing the request. + readonly attribute boolean isReload; + + [Throws] void respondWith(Promise r); + Promise forwardTo(USVString url); + Promise default(); +}; + +dictionary FetchEventInit : EventInit { + Request request; + ServiceWorkerClient client; + boolean isReload; +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 6cd53553419c..4cdf32fcff68 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -132,6 +132,7 @@ WEBIDL_FILES = [ 'EventTarget.webidl', 'ExtendableEvent.webidl', 'Fetch.webidl', + 'FetchEvent.webidl', 'File.webidl', 'FileList.webidl', 'FileMode.webidl', diff --git a/dom/workers/ServiceWorkerEvents.cpp b/dom/workers/ServiceWorkerEvents.cpp index 2e53e4d6d598..6199f3b5f405 100644 --- a/dom/workers/ServiceWorkerEvents.cpp +++ b/dom/workers/ServiceWorkerEvents.cpp @@ -5,9 +5,20 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ServiceWorkerEvents.h" +#include "ServiceWorkerClient.h" +#include "nsINetworkInterceptController.h" +#include "nsIOutputStream.h" #include "nsContentUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsStreamUtils.h" +#include "nsNetCID.h" +#include "mozilla/dom/FetchEventBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/Response.h" #include "mozilla/dom/WorkerScope.h" #include "mozilla/dom/workers/bindings/ServiceWorker.h" @@ -15,6 +26,271 @@ using namespace mozilla::dom; BEGIN_WORKERS_NAMESPACE +FetchEvent::FetchEvent(EventTarget* aOwner) +: Event(aOwner, nullptr, nullptr) +, mWindowId(0) +, mIsReload(false) +, mWaitToRespond(false) +{ +} + +FetchEvent::~FetchEvent() +{ +} + +void +FetchEvent::PostInit(nsMainThreadPtrHandle& aChannel, + nsMainThreadPtrHandle& aServiceWorker, + uint64_t aWindowId) +{ + mChannel = aChannel; + mServiceWorker = aServiceWorker; + mWindowId = aWindowId; +} + +/*static*/ already_AddRefed +FetchEvent::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const FetchEventInit& aOptions, + ErrorResult& aRv) +{ + nsRefPtr owner = do_QueryObject(aGlobal.GetAsSupports()); + MOZ_ASSERT(owner); + nsRefPtr e = new FetchEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aOptions.mBubbles, aOptions.mCancelable); + e->SetTrusted(trusted); + e->mRequest = aOptions.mRequest.WasPassed() ? + &aOptions.mRequest.Value() : nullptr; + e->mIsReload = aOptions.mIsReload.WasPassed() ? + aOptions.mIsReload.Value() : false; + e->mClient = aOptions.mClient.WasPassed() ? + &aOptions.mClient.Value() : nullptr; + return e.forget(); +} + +namespace { + +class CancelChannelRunnable MOZ_FINAL : public nsRunnable +{ + nsMainThreadPtrHandle mChannel; +public: + explicit CancelChannelRunnable(nsMainThreadPtrHandle& aChannel) + : mChannel(aChannel) + { + } + + NS_IMETHOD Run() + { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = mChannel->Cancel(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } +}; + +class FinishResponse MOZ_FINAL : public nsRunnable +{ + nsMainThreadPtrHandle mChannel; +public: + explicit FinishResponse(nsMainThreadPtrHandle& aChannel) + : mChannel(aChannel) + { + } + + NS_IMETHOD + Run() + { + AssertIsOnMainThread(); + nsresult rv = mChannel->FinishSynthesizedResponse(); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to finish synthesized response"); + return rv; + } +}; + +class RespondWithHandler MOZ_FINAL : public PromiseNativeHandler +{ + nsMainThreadPtrHandle mInterceptedChannel; + nsMainThreadPtrHandle mServiceWorker; +public: + RespondWithHandler(nsMainThreadPtrHandle& aChannel, + nsMainThreadPtrHandle& aServiceWorker) + : mInterceptedChannel(aChannel) + , mServiceWorker(aServiceWorker) + { + } + + void ResolvedCallback(JSContext* aCx, JS::Handle aValue) MOZ_OVERRIDE; + + void RejectedCallback(JSContext* aCx, JS::Handle aValue) MOZ_OVERRIDE; + + void CancelRequest(); +}; + +struct RespondWithClosure +{ + nsMainThreadPtrHandle mInterceptedChannel; + + explicit RespondWithClosure(nsMainThreadPtrHandle& aChannel) + : mInterceptedChannel(aChannel) + { + } +}; + +void RespondWithCopyComplete(void* aClosure, nsresult aStatus) +{ + nsAutoPtr data(static_cast(aClosure)); + nsCOMPtr event; + if (NS_SUCCEEDED(aStatus)) { + event = new FinishResponse(data->mInterceptedChannel); + } else { + event = new CancelChannelRunnable(data->mInterceptedChannel); + } + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(event))); +} + +class MOZ_STACK_CLASS AutoCancel +{ + nsRefPtr mOwner; + +public: + explicit AutoCancel(RespondWithHandler* aOwner) + : mOwner(aOwner) + { + } + + ~AutoCancel() + { + if (mOwner) { + mOwner->CancelRequest(); + } + } + + void Reset() + { + mOwner = nullptr; + } +}; + +void +RespondWithHandler::ResolvedCallback(JSContext* aCx, JS::Handle aValue) +{ + AutoCancel autoCancel(this); + + if (!aValue.isObject()) { + return; + } + + nsRefPtr response; + nsresult rv = UNWRAP_OBJECT(Response, &aValue.toObject(), response); + if (NS_FAILED(rv)) { + return; + } + + nsCOMPtr body; + response->GetBody(getter_AddRefs(body)); + if (NS_WARN_IF(!body) || NS_WARN_IF(response->BodyUsed())) { + return; + } + response->SetBodyUsed(); + + nsCOMPtr responseBody; + rv = mInterceptedChannel->GetResponseBody(getter_AddRefs(responseBody)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsAutoPtr closure(new RespondWithClosure(mInterceptedChannel)); + + nsCOMPtr stsThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(!stsThread)) { + return; + } + rv = NS_AsyncCopy(body, responseBody, stsThread, NS_ASYNCCOPY_VIA_READSEGMENTS, 4096, + RespondWithCopyComplete, closure.forget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + autoCancel.Reset(); +} + +void +RespondWithHandler::RejectedCallback(JSContext* aCx, JS::Handle aValue) +{ + CancelRequest(); +} + +void +RespondWithHandler::CancelRequest() +{ + nsCOMPtr runnable = new CancelChannelRunnable(mInterceptedChannel); + NS_DispatchToMainThread(runnable); +} + +} // anonymous namespace + +void +FetchEvent::RespondWith(Promise& aPromise, ErrorResult& aRv) +{ + if (mWaitToRespond) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mWaitToRespond = true; + nsRefPtr handler = new RespondWithHandler(mChannel, mServiceWorker); + aPromise.AppendNativeHandler(handler); +} + +already_AddRefed +FetchEvent::Client() +{ + if (!mClient) { + mClient = new ServiceWorkerClient(GetParentObject(), mWindowId); + } + nsRefPtr client = mClient; + return client.forget(); +} + +already_AddRefed +FetchEvent::ForwardTo(const nsAString& aUrl) +{ + nsCOMPtr global = do_QueryInterface(GetParentObject()); + MOZ_ASSERT(global); + ErrorResult result; + nsRefPtr promise = Promise::Create(global, result); + if (NS_WARN_IF(result.Failed())) { + return nullptr; + } + + promise->MaybeReject(NS_ERROR_NOT_AVAILABLE); + return promise.forget(); +} + +already_AddRefed +FetchEvent::Default() +{ + nsCOMPtr global = do_QueryInterface(GetParentObject()); + MOZ_ASSERT(global); + ErrorResult result; + nsRefPtr promise = Promise::Create(global, result); + if (result.Failed()) { + return nullptr; + } + + promise->MaybeReject(NS_ERROR_NOT_AVAILABLE); + return promise.forget(); +} + +NS_IMPL_ADDREF_INHERITED(FetchEvent, Event) +NS_IMPL_RELEASE_INHERITED(FetchEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(FetchEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FetchEvent, Event, mRequest, mClient) + ExtendableEvent::ExtendableEvent(EventTarget* aOwner) : Event(aOwner, nullptr, nullptr) { diff --git a/dom/workers/ServiceWorkerEvents.h b/dom/workers/ServiceWorkerEvents.h index 04f30ccef88c..7a20cc87995c 100644 --- a/dom/workers/ServiceWorkerEvents.h +++ b/dom/workers/ServiceWorkerEvents.h @@ -8,12 +8,87 @@ #include "mozilla/dom/Event.h" #include "mozilla/dom/ExtendableEventBinding.h" +#include "mozilla/dom/FetchEventBinding.h" #include "mozilla/dom/InstallEventBinding.h" #include "mozilla/dom/Promise.h" +#include "nsProxyRelease.h" + +class nsIInterceptedChannel; + +namespace mozilla { +namespace dom { + class Request; +} // namespace dom +} // namespace mozilla BEGIN_WORKERS_NAMESPACE class ServiceWorker; +class ServiceWorkerClient; + +class FetchEvent MOZ_FINAL : public Event +{ + nsMainThreadPtrHandle mChannel; + nsMainThreadPtrHandle mServiceWorker; + nsRefPtr mClient; + nsRefPtr mRequest; + uint64_t mWindowId; + bool mIsReload; + bool mWaitToRespond; +protected: + explicit FetchEvent(EventTarget* aOwner); + ~FetchEvent(); + +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FetchEvent, Event) + NS_FORWARD_TO_EVENT + + virtual JSObject* WrapObjectInternal(JSContext* aCx) MOZ_OVERRIDE + { + return FetchEventBinding::Wrap(aCx, this); + } + + void PostInit(nsMainThreadPtrHandle& aChannel, + nsMainThreadPtrHandle& aServiceWorker, + uint64_t aWindowId); + + static already_AddRefed + Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const FetchEventInit& aOptions, + ErrorResult& aRv); + + bool + WaitToRespond() const + { + return mWaitToRespond; + } + + Request* + Request_() const + { + return mRequest; + } + + already_AddRefed + Client(); + + bool + IsReload() const + { + return mIsReload; + } + + void + RespondWith(Promise& aPromise, ErrorResult& aRv); + + already_AddRefed + ForwardTo(const nsAString& aUrl); + + already_AddRefed + Default(); +}; class ExtendableEvent : public Event { diff --git a/dom/workers/ServiceWorkerManager.cpp b/dom/workers/ServiceWorkerManager.cpp index c1dd85644f23..208a8144d055 100644 --- a/dom/workers/ServiceWorkerManager.cpp +++ b/dom/workers/ServiceWorkerManager.cpp @@ -11,7 +11,10 @@ #include "nsIStreamLoader.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsINetworkInterceptController.h" #include "nsPIDOMWindow.h" +#include "nsDebug.h" #include "jsapi.h" @@ -19,9 +22,13 @@ #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/DOMError.h" #include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/Headers.h" #include "mozilla/dom/InstallEventBinding.h" +#include "mozilla/dom/InternalHeaders.h" #include "mozilla/dom/Navigator.h" #include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/RootedDictionary.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" @@ -2121,6 +2128,267 @@ ServiceWorkerManager::GetServiceWorkerForScope(nsIDOMWindow* aWindow, return NS_OK; } +class FetchEventRunnable : public WorkerRunnable + , public nsIHttpHeaderVisitor { + nsMainThreadPtrHandle mInterceptedChannel; + nsMainThreadPtrHandle mServiceWorker; + nsTArray mHeaderNames; + nsTArray mHeaderValues; + uint64_t mWindowId; + nsCString mSpec; + nsCString mMethod; + bool mIsReload; +public: + FetchEventRunnable(WorkerPrivate* aWorkerPrivate, + nsMainThreadPtrHandle& aChannel, + nsMainThreadPtrHandle& aServiceWorker, + uint64_t aWindowId) + : WorkerRunnable(aWorkerPrivate, WorkerThreadModifyBusyCount) + , mInterceptedChannel(aChannel) + , mServiceWorker(aServiceWorker) + , mWindowId(aWindowId) + { + MOZ_ASSERT(aWorkerPrivate); + } + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) + { + mHeaderNames.AppendElement(aHeader); + mHeaderValues.AppendElement(aValue); + return NS_OK; + } + + nsresult + Init() + { + nsCOMPtr channel; + nsresult rv = mInterceptedChannel->GetChannel(getter_AddRefs(channel)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr uri; + rv = channel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = uri->GetSpec(mSpec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr httpChannel = do_QueryInterface(channel); + NS_ENSURE_TRUE(httpChannel, NS_ERROR_NOT_AVAILABLE); + + rv = httpChannel->GetRequestMethod(mMethod); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t loadFlags; + rv = channel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + //TODO(jdm): we should probably include reload-ness in the loadinfo or as a separate load flag + mIsReload = false; + + rv = httpChannel->VisitRequestHeaders(this); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + bool + WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aWorkerPrivate); + return DispatchFetchEvent(aCx, aWorkerPrivate); + } + +private: + ~FetchEventRunnable() {} + + class ResumeRequest MOZ_FINAL : public nsRunnable { + nsMainThreadPtrHandle mChannel; + public: + explicit ResumeRequest(nsMainThreadPtrHandle& aChannel) + : mChannel(aChannel) + { + } + + NS_IMETHOD Run() + { + AssertIsOnMainThread(); + nsresult rv = mChannel->ResetInterception(); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to resume intercepted network request"); + return rv; + } + }; + + bool + DispatchFetchEvent(JSContext* aCx, WorkerPrivate* aWorkerPrivate) + { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aWorkerPrivate); + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); + GlobalObject globalObj(aCx, aWorkerPrivate->GlobalScope()->GetWrapper()); + + RequestOrUSVString requestInfo; + *requestInfo.SetAsUSVString().ToAStringPtr() = NS_ConvertUTF8toUTF16(mSpec); + + RootedDictionary reqInit(aCx); + reqInit.mMethod.Construct(mMethod); + + nsRefPtr internalHeaders = new InternalHeaders(HeadersGuardEnum::Request); + MOZ_ASSERT(mHeaderNames.Length() == mHeaderValues.Length()); + for (uint32_t i = 0; i < mHeaderNames.Length(); i++) { + ErrorResult rv; + internalHeaders->Set(mHeaderNames[i], mHeaderValues[i], rv); + if (NS_WARN_IF(rv.Failed())) { + return false; + } + } + + nsRefPtr headers = new Headers(globalObj.GetAsSupports(), internalHeaders); + reqInit.mHeaders.Construct(); + reqInit.mHeaders.Value().SetAsHeaders() = headers; + + //TODO(jdm): set request body + //TODO(jdm): set request same-origin mode and credentials + + ErrorResult rv; + nsRefPtr request = Request::Constructor(globalObj, requestInfo, reqInit, rv); + if (NS_WARN_IF(rv.Failed())) { + return false; + } + + RootedDictionary init(aCx); + init.mRequest.Construct(); + init.mRequest.Value() = request; + init.mBubbles = false; + init.mCancelable = true; + init.mIsReload.Construct(mIsReload); + nsRefPtr event = + FetchEvent::Constructor(globalObj, NS_LITERAL_STRING("fetch"), init, rv); + if (NS_WARN_IF(rv.Failed())) { + return false; + } + + event->PostInit(mInterceptedChannel, mServiceWorker, mWindowId); + event->SetTrusted(true); + + nsRefPtr target = do_QueryObject(aWorkerPrivate->GlobalScope()); + nsresult rv2 = target->DispatchDOMEvent(nullptr, event, nullptr, nullptr); + if (NS_WARN_IF(NS_FAILED(rv2)) || !event->WaitToRespond()) { + nsCOMPtr runnable = new ResumeRequest(mInterceptedChannel); + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(runnable))); + } + return true; + } +}; + +NS_IMPL_ISUPPORTS_INHERITED(FetchEventRunnable, WorkerRunnable, nsIHttpHeaderVisitor) + +NS_IMETHODIMP +ServiceWorkerManager::DispatchFetchEvent(nsIDocument* aDoc, nsIInterceptedChannel* aChannel) +{ + MOZ_ASSERT(aChannel); + nsCOMPtr serviceWorker; + + bool isNavigation = false; + nsresult rv = aChannel->GetIsNavigation(&isNavigation); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isNavigation) { + MOZ_ASSERT(aDoc); + rv = GetDocumentController(aDoc->GetWindow(), getter_AddRefs(serviceWorker)); + } else { + nsCOMPtr internalChannel; + rv = aChannel->GetChannel(getter_AddRefs(internalChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr uri; + rv = internalChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsRefPtr registration = + GetServiceWorkerRegistrationInfo(uri); + // This should only happen if IsAvailableForURI() returned true. + MOZ_ASSERT(registration); + MOZ_ASSERT(registration->mActiveWorker); + + nsRefPtr sw; + rv = CreateServiceWorker(registration->mPrincipal, + registration->mActiveWorker->ScriptSpec(), + registration->mScope, + getter_AddRefs(sw)); + serviceWorker = sw.forget(); + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsMainThreadPtrHandle handle( + new nsMainThreadPtrHolder(aChannel, false)); + + uint64_t windowId = aDoc ? aDoc->GetInnerWindow()->WindowID() : 0; + + nsRefPtr sw = static_cast(serviceWorker.get()); + nsMainThreadPtrHandle serviceWorkerHandle( + new nsMainThreadPtrHolder(sw)); + + nsRefPtr event = + new FetchEventRunnable(sw->GetWorkerPrivate(), handle, serviceWorkerHandle, windowId); + rv = event->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + AutoJSAPI api; + api.Init(); + if (NS_WARN_IF(!event->Dispatch(api.cx()))) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::IsAvailableForURI(nsIURI* aURI, bool* aIsAvailable) +{ + MOZ_ASSERT(aURI); + MOZ_ASSERT(aIsAvailable); + nsRefPtr registration = + GetServiceWorkerRegistrationInfo(aURI); + *aIsAvailable = registration && registration->mActiveWorker; + return NS_OK; +} + +NS_IMETHODIMP +ServiceWorkerManager::IsControlled(nsIDocument* aDoc, bool* aIsControlled) +{ + MOZ_ASSERT(aDoc); + MOZ_ASSERT(aIsControlled); + nsRefPtr registration; + nsresult rv = GetDocumentRegistration(aDoc, getter_AddRefs(registration)); + NS_ENSURE_SUCCESS(rv, rv); + *aIsControlled = !!registration; + return NS_OK; +} + +nsresult +ServiceWorkerManager::GetDocumentRegistration(nsIDocument* aDoc, + ServiceWorkerRegistrationInfo** aRegistrationInfo) +{ + nsRefPtr registration; + if (!mControlledDocuments.Get(aDoc, getter_AddRefs(registration))) { + return NS_ERROR_FAILURE; + } + + // If the document is controlled, the current worker MUST be non-null. + if (!registration->mActiveWorker) { + return NS_ERROR_NOT_AVAILABLE; + } + + registration.forget(aRegistrationInfo); + return NS_OK; +} + /* * The .controller is for the registration associated with the document when * the document was loaded. @@ -2137,21 +2405,16 @@ ServiceWorkerManager::GetDocumentController(nsIDOMWindow* aWindow, nsISupports** nsCOMPtr doc = window->GetExtantDoc(); nsRefPtr registration; - if (!mControlledDocuments.Get(doc, getter_AddRefs(registration))) { - return NS_ERROR_FAILURE; + nsresult rv = GetDocumentRegistration(doc, getter_AddRefs(registration)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; } - // If the document is controlled, the current worker MUST be non-null. - if (!registration->mActiveWorker) { - return NS_ERROR_NOT_AVAILABLE; - } - - nsRefPtr serviceWorker; - nsresult rv = CreateServiceWorkerForWindow(window, - registration->mActiveWorker->ScriptSpec(), - registration->mScope, - getter_AddRefs(serviceWorker)); + rv = CreateServiceWorkerForWindow(window, + registration->mActiveWorker->ScriptSpec(), + registration->mScope, + getter_AddRefs(serviceWorker)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } diff --git a/dom/workers/ServiceWorkerManager.h b/dom/workers/ServiceWorkerManager.h index e642e08ed9c9..c5f8ff5c8b17 100644 --- a/dom/workers/ServiceWorkerManager.h +++ b/dom/workers/ServiceWorkerManager.h @@ -399,6 +399,9 @@ private: nsresult Update(ServiceWorkerRegistrationInfo* aRegistration); + nsresult + GetDocumentRegistration(nsIDocument* aDoc, ServiceWorkerRegistrationInfo** aRegistrationInfo); + NS_IMETHOD CreateServiceWorkerForWindow(nsPIDOMWindow* aWindow, const nsACString& aScriptSpec, diff --git a/dom/workers/test/serviceworkers/fetch/fetch_tests.js b/dom/workers/test/serviceworkers/fetch/fetch_tests.js new file mode 100644 index 000000000000..e6900b6860d3 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/fetch_tests.js @@ -0,0 +1,54 @@ +function fetch(name, onload, onerror, headers) { + expectAsyncResult(); + + onload = onload || function() { + my_ok(false, "XHR load should not complete successfully"); + finish(); + }; + onerror = onerror || function() { + my_ok(false, "XHR load should be intercepted successfully"); + finish(); + }; + + var x = new XMLHttpRequest(); + x.open('GET', name, true); + x.onload = function() { onload(x) }; + x.onerror = function() { onerror(x) }; + headers = headers || []; + headers.forEach(function(header) { + x.setRequestHeader(header[0], header[1]); + }); + x.send(); +} + +fetch('synthesized.txt', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "synthesized response body", "load should have synthesized response"); + finish(); +}); + +fetch('ignored.txt', function(xhr) { + my_ok(xhr.status == 404, "load should be uninterrupted"); + finish(); +}); + +fetch('rejected.txt', null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetch('nonresponse.txt', null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetch('nonresponse2.txt', null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetch('headers.txt', function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "1", "request header checks should have passed"); + finish(); +}, null, [["X-Test1", "header1"], ["X-Test2", "header2"]]); diff --git a/dom/workers/test/serviceworkers/fetch/fetch_worker_script.js b/dom/workers/test/serviceworkers/fetch/fetch_worker_script.js new file mode 100644 index 000000000000..61efb647c8f9 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/fetch_worker_script.js @@ -0,0 +1,29 @@ +function my_ok(v, msg) { + postMessage({type: "ok", value: v, msg: msg}); +} + +function finish() { + postMessage('finish'); +} + +function expectAsyncResult() { + postMessage('expect'); +} + +expectAsyncResult(); +try { + var success = false; + importScripts("nonexistent_imported_script.js"); +} catch(x) { +} + +my_ok(success, "worker imported script should be intercepted"); +finish(); + +function check_intercepted_script() { + success = true; +} + +importScripts('fetch_tests.js') + +finish(); //corresponds to the gExpected increment before creating this worker diff --git a/dom/workers/test/serviceworkers/fetch/index.html b/dom/workers/test/serviceworkers/fetch/index.html new file mode 100644 index 000000000000..88962f798956 --- /dev/null +++ b/dom/workers/test/serviceworkers/fetch/index.html @@ -0,0 +1,147 @@ + + + + + Bug 94048 - test install event. + + + + +

+ +
+

+
+
+
+
+
+
diff --git a/dom/workers/test/serviceworkers/fetch_event_worker.js b/dom/workers/test/serviceworkers/fetch_event_worker.js
new file mode 100644
index 000000000000..107544828397
--- /dev/null
+++ b/dom/workers/test/serviceworkers/fetch_event_worker.js
@@ -0,0 +1,86 @@
+onfetch = function(ev) {
+  if (ev.request.url.contains("synthesized.txt")) {
+    var p = new Promise(function(resolve) {
+      var r = new Response("synthesized response body", {});
+      resolve(r);
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("ignored.txt")) {
+  }
+
+  else if (ev.request.url.contains("rejected.txt")) {
+    var p = new Promise(function(resolve, reject) {
+      reject();
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("nonresponse.txt")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve(5);
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("nonresponse2.txt")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve({});
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("headers.txt")) {
+    var p = new Promise(function(resolve, reject) {
+      var ok = true;
+      ok &= ev.request.headers.get("X-Test1") == "header1";
+      ok &= ev.request.headers.get("X-Test2") == "header2";
+      var r = new Response(ok.toString(), {});
+      resolve(r);
+    });
+    ev.respondWith(p);    
+  }
+
+  else if (ev.request.url.contains("nonexistent_image.gif")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve(new Response(atob("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs"), {}));
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("nonexistent_script.js")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve(new Response("check_intercepted_script();", {}));
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("nonexistent_stylesheet.css")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve(new Response("#style-test { background-color: black !important; }", {}));
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("nonexistent_page.html")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve(new Response("", {}));
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("nonexistent_worker_script.js")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve(new Response("postMessage('worker-intercept-success')", {}));
+    });
+    ev.respondWith(p);
+  }
+
+  else if (ev.request.url.contains("nonexistent_imported_script.js")) {
+    var p = new Promise(function(resolve, reject) {
+      resolve(new Response("check_intercepted_script();", {}));
+    });
+    ev.respondWith(p);
+  }
+}
diff --git a/dom/workers/test/serviceworkers/mochitest.ini b/dom/workers/test/serviceworkers/mochitest.ini
index edc7d06c7420..074cba12d1d7 100644
--- a/dom/workers/test/serviceworkers/mochitest.ini
+++ b/dom/workers/test/serviceworkers/mochitest.ini
@@ -4,6 +4,7 @@ support-files =
   worker.js
   worker2.js
   worker3.js
+  fetch_event_worker.js
   parse_error_worker.js
   activate_event_error_worker.js
   install_event_worker.js
@@ -20,10 +21,14 @@ support-files =
   worker_unregister.js
   worker_update.js
   message_posting_worker.js
+  fetch/index.html
+  fetch/fetch_worker_script.js
+  fetch/fetch_tests.js
 
 [test_unregister.html]
 skip-if = true # Bug 1133805
 [test_installation_simple.html]
+[test_fetch_event.html]
 [test_get_serviced.html]
 [test_install_event.html]
 [test_navigator.html]
diff --git a/dom/workers/test/serviceworkers/test_fetch_event.html b/dom/workers/test/serviceworkers/test_fetch_event.html
new file mode 100644
index 000000000000..090e6e4faf68
--- /dev/null
+++ b/dom/workers/test/serviceworkers/test_fetch_event.html
@@ -0,0 +1,61 @@
+
+
+
+
+  Bug 94048 - test install event.
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/netwerk/base/nsINetworkInterceptController.idl b/netwerk/base/nsINetworkInterceptController.idl
index 766e2cd9266a..576082c24557 100644
--- a/netwerk/base/nsINetworkInterceptController.idl
+++ b/netwerk/base/nsINetworkInterceptController.idl
@@ -5,7 +5,7 @@
 
 #include "nsISupports.idl"
 
-interface nsIHttpChannelInternal;
+interface nsIChannel;
 interface nsIOutputStream;
 interface nsIURI;
 
@@ -16,7 +16,7 @@ interface nsIURI;
  * which do not implement nsIChannel.
  */
 
-[scriptable, uuid(0b5f82a7-5824-4a0d-bf5c-8a8a7684c0c8)]
+[scriptable, uuid(9d127b63-dfad-484d-a0e1-cb82697a095b)]
 interface nsIInterceptedChannel : nsISupports
 {
     /**
@@ -37,6 +37,28 @@ interface nsIInterceptedChannel : nsISupports
      * after this point.
      */
     void finishSynthesizedResponse();
+
+    /**
+     * Cancel the pending intercepted request.
+     * @return NS_ERROR_FAILURE if the response has already been synthesized or
+     *         the original request has been instructed to continue.
+     */
+    void cancel();
+
+    /**
+     * The synthesized response body to be produced.
+     */
+    readonly attribute nsIOutputStream responseBody;
+
+    /**
+     * The underlying channel object that was intercepted.
+     */
+    readonly attribute nsIChannel channel;
+
+    /**
+     * True if the underlying request was caused by a navigation attempt.
+     */
+    readonly attribute bool isNavigation;
 };
 
 /**
@@ -45,7 +67,7 @@ interface nsIInterceptedChannel : nsISupports
  * request should be intercepted before any network request is initiated.
  */
 
-[scriptable, uuid(b3ad3e9b-91d8-44d0-a0c5-dc2e9374f599)]
+[scriptable, uuid(69150b77-b561-43a2-bfba-7301dd5a35d0)]
 interface nsINetworkInterceptController : nsISupports
 {
     /**
@@ -53,15 +75,15 @@ interface nsINetworkInterceptController : nsISupports
      * requests until specifically instructed to do so.
      *
      * @param aURI the URI being requested by a channel
+     * @param aIsNavigate True if the request is for a navigation, false for a fetch.
      */
-    bool shouldPrepareForIntercept(in nsIURI aURI);
+    bool shouldPrepareForIntercept(in nsIURI aURI, in bool aIsNavigate);
 
     /**
      * Notification when a given intercepted channel is prepared to accept a synthesized
      * response via the provided stream.
      *
      * @param aChannel the controlling interface for a channel that has been intercepted
-     * @param aStream a stream directly into the channel's synthesized response body
      */
-    void channelIntercepted(in nsIInterceptedChannel aChannel, in nsIOutputStream aStream);
+    void channelIntercepted(in nsIInterceptedChannel aChannel);
 };
diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp
index a8786f2c6ed6..69a3594179e0 100644
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -1933,6 +1933,12 @@ HttpBaseChannel::GetURIPrincipal()
   return mPrincipal;
 }
 
+bool
+HttpBaseChannel::IsNavigation()
+{
+  return mLoadFlags & LOAD_DOCUMENT_URI;
+}
+
 bool
 HttpBaseChannel::ShouldIntercept()
 {
@@ -1940,7 +1946,9 @@ HttpBaseChannel::ShouldIntercept()
   GetCallback(controller);
   bool shouldIntercept = false;
   if (controller && !mForceNoIntercept) {
-    nsresult rv = controller->ShouldPrepareForIntercept(mURI, &shouldIntercept);
+    nsresult rv = controller->ShouldPrepareForIntercept(mURI,
+                                                        IsNavigation(),
+                                                        &shouldIntercept);
     NS_ENSURE_SUCCESS(rv, false);
   }
   return shouldIntercept;
diff --git a/netwerk/protocol/http/HttpBaseChannel.h b/netwerk/protocol/http/HttpBaseChannel.h
index a26bc2df96e6..de7d0ba26553 100644
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -241,7 +241,7 @@ public:
     const NetAddr& GetPeerAddr() { return mPeerAddr; }
 
 public: /* Necko internal use only... */
-
+    bool IsNavigation();
 
     // Return whether upon a redirect code of httpStatus for method, the
     // request method should be rewritten to GET.
diff --git a/netwerk/protocol/http/HttpChannelChild.cpp b/netwerk/protocol/http/HttpChannelChild.cpp
index 741d0e4ae007..944894343dac 100644
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -56,6 +56,111 @@ static_assert(FileDescriptorSet::MAX_DESCRIPTORS_PER_MESSAGE == 250,
 
 }
 
+// A stream listener interposed between the nsInputStreamPump used for intercepted channels
+// and this channel's original listener. This is only used to ensure the original listener
+// sees the channel as the request object, and to synthesize OnStatus and OnProgress notifications.
+class InterceptStreamListener : public nsIStreamListener
+                              , public nsIProgressEventSink
+{
+  nsRefPtr mOwner;
+  nsCOMPtr mContext;
+  virtual ~InterceptStreamListener() {}
+ public:
+  InterceptStreamListener(HttpChannelChild* aOwner, nsISupports* aContext)
+  : mOwner(aOwner)
+  , mContext(aContext)
+  {
+  }
+
+  NS_DECL_ISUPPORTS
+  NS_DECL_NSIREQUESTOBSERVER
+  NS_DECL_NSISTREAMLISTENER
+  NS_DECL_NSIPROGRESSEVENTSINK
+
+  void Cleanup();
+};
+
+NS_IMPL_ISUPPORTS(InterceptStreamListener,
+                  nsIStreamListener,
+                  nsIRequestObserver,
+                  nsIProgressEventSink)
+
+NS_IMETHODIMP
+InterceptStreamListener::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext)
+{
+  if (mOwner) {
+    mOwner->DoOnStartRequest(mOwner, mContext);
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptStreamListener::OnStatus(nsIRequest* aRequest, nsISupports* aContext,
+                                  nsresult status, const char16_t* aStatusArg)
+{
+  if (mOwner) {
+    mOwner->DoOnStatus(mOwner, status);
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptStreamListener::OnProgress(nsIRequest* aRequest, nsISupports* aContext,
+                                    int64_t aProgress, int64_t aProgressMax)
+{
+  if (mOwner) {
+    mOwner->DoOnProgress(mOwner, aProgress, aProgressMax);
+  }
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptStreamListener::OnDataAvailable(nsIRequest* aRequest, nsISupports* aContext,
+                                         nsIInputStream* aInputStream, uint64_t aOffset,
+                                         uint32_t aCount)
+{
+  if (!mOwner) {
+    return NS_OK;
+  }
+
+  uint32_t loadFlags;
+  mOwner->GetLoadFlags(&loadFlags);
+
+  if (!(loadFlags & HttpBaseChannel::LOAD_BACKGROUND)) {
+    nsCOMPtr uri;
+    mOwner->GetURI(getter_AddRefs(uri));
+
+    nsAutoCString host;
+    uri->GetHost(host);
+
+    OnStatus(mOwner, aContext, NS_NET_STATUS_READING, NS_ConvertUTF8toUTF16(host).get());
+
+    int64_t progress = aOffset + aCount;
+    OnProgress(mOwner, aContext, progress, mOwner->GetResponseHead()->ContentLength());
+  }
+
+  mOwner->DoOnDataAvailable(mOwner, mContext, aInputStream, aOffset, aCount);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptStreamListener::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatusCode)
+{
+  if (mOwner) {
+    mOwner->DoPreOnStopRequest(aStatusCode);
+    mOwner->DoOnStopRequest(mOwner, mContext);
+  }
+  Cleanup();
+  return NS_OK;
+}
+
+void
+InterceptStreamListener::Cleanup()
+{
+  mOwner = nullptr;
+  mContext = nullptr;
+}
+
 //-----------------------------------------------------------------------------
 // HttpChannelChild
 //-----------------------------------------------------------------------------
@@ -852,6 +957,10 @@ HttpChannelChild::DoNotifyListenerCleanup()
   LOG(("HttpChannelChild::DoNotifyListenerCleanup [this=%p]\n", this));
   if (mIPCOpen)
     PHttpChannelChild::Send__delete__(this);
+  if (mInterceptListener) {
+    mInterceptListener->Cleanup();
+    mInterceptListener = nullptr;
+  }
 }
 
 class DeleteSelfEvent : public ChannelEvent
@@ -1237,6 +1346,10 @@ HttpChannelChild::Cancel(nsresult status)
     mStatus = status;
     if (RemoteChannelExists())
       SendCancel(status);
+    if (mSynthesizedResponsePump) {
+      mSynthesizedResponsePump->Cancel(status);
+    }
+    mInterceptListener = nullptr;
   }
   return NS_OK;
 }
@@ -1343,89 +1456,6 @@ HttpChannelChild::GetSecurityInfo(nsISupports **aSecurityInfo)
   return NS_OK;
 }
 
-// A stream listener interposed between the nsInputStreamPump used for intercepted channels
-// and this channel's original listener. This is only used to ensure the original listener
-// sees the channel as the request object, and to synthesize OnStatus and OnProgress notifications.
-class InterceptStreamListener : public nsIStreamListener
-                              , public nsIProgressEventSink
-{
-  nsRefPtr mOwner;
-  nsCOMPtr mContext;
-  virtual ~InterceptStreamListener() {}
-public:
-  InterceptStreamListener(HttpChannelChild* aOwner, nsISupports* aContext)
-  : mOwner(aOwner)
-  , mContext(aContext)
-  {
-  }
-
-  NS_DECL_ISUPPORTS
-  NS_DECL_NSIREQUESTOBSERVER
-  NS_DECL_NSISTREAMLISTENER
-  NS_DECL_NSIPROGRESSEVENTSINK
-};
-
-NS_IMPL_ISUPPORTS(InterceptStreamListener,
-                  nsIStreamListener,
-                  nsIRequestObserver,
-                  nsIProgressEventSink)
-
-NS_IMETHODIMP
-InterceptStreamListener::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext)
-{
-  mOwner->DoOnStartRequest(mOwner, mContext);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-InterceptStreamListener::OnStatus(nsIRequest* aRequest, nsISupports* aContext,
-                                  nsresult status, const char16_t* aStatusArg)
-{
-  mOwner->DoOnStatus(mOwner, status);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-InterceptStreamListener::OnProgress(nsIRequest* aRequest, nsISupports* aContext,
-                                    int64_t aProgress, int64_t aProgressMax)
-{
-  mOwner->DoOnProgress(mOwner, aProgress, aProgressMax);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-InterceptStreamListener::OnDataAvailable(nsIRequest* aRequest, nsISupports* aContext,
-                                         nsIInputStream* aInputStream, uint64_t aOffset,
-                                         uint32_t aCount)
-{
-  uint32_t loadFlags;
-  mOwner->GetLoadFlags(&loadFlags);
-
-  if (!(loadFlags & HttpBaseChannel::LOAD_BACKGROUND)) {
-    nsCOMPtr uri;
-    mOwner->GetURI(getter_AddRefs(uri));
-
-    nsAutoCString host;
-    uri->GetHost(host);
-
-    OnStatus(mOwner, aContext, NS_NET_STATUS_READING, NS_ConvertUTF8toUTF16(host).get());
-
-    int64_t progress = aOffset + aCount;
-    OnProgress(mOwner, aContext, progress, mOwner->GetResponseHead()->ContentLength());
-  }
-
-  mOwner->DoOnDataAvailable(mOwner, mContext, aInputStream, aOffset, aCount);
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-InterceptStreamListener::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatusCode)
-{
-  mOwner->DoPreOnStopRequest(aStatusCode);
-  mOwner->DoOnStopRequest(mOwner, mContext);
-  return NS_OK;
-}
-
 NS_IMETHODIMP
 HttpChannelChild::AsyncOpen(nsIStreamListener *listener, nsISupports *aContext)
 {
@@ -2043,6 +2073,7 @@ HttpChannelChild::DivertToParent(ChannelDiverterChild **aChild)
 void
 HttpChannelChild::ResetInterception()
 {
+  mInterceptListener->Cleanup();
   mInterceptListener = nullptr;
 
   // Continue with the original cross-process request
@@ -2051,7 +2082,7 @@ HttpChannelChild::ResetInterception()
 }
 
 void
-HttpChannelChild::OverrideWithSynthesizedResponse(nsHttpResponseHead* aResponseHead,
+HttpChannelChild::OverrideWithSynthesizedResponse(nsAutoPtr& aResponseHead,
                                                   nsInputStreamPump* aPump)
 {
   mSynthesizedResponsePump = aPump;
@@ -2063,6 +2094,10 @@ HttpChannelChild::OverrideWithSynthesizedResponse(nsHttpResponseHead* aResponseH
     nsresult rv = mSynthesizedResponsePump->Suspend();
     NS_ENSURE_SUCCESS_VOID(rv);
   }
+
+  if (mCanceled) {
+    mSynthesizedResponsePump->Cancel(mStatus);
+  }
 }
 
 }} // mozilla::net
diff --git a/netwerk/protocol/http/HttpChannelChild.h b/netwerk/protocol/http/HttpChannelChild.h
index 53857a54169a..4bca9eaa1282 100644
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -159,7 +159,7 @@ private:
 
   // Override this channel's pending response with a synthesized one. The content will be
   // asynchronously read from the pump.
-  void OverrideWithSynthesizedResponse(nsHttpResponseHead* aResponseHead, nsInputStreamPump* aPump);
+  void OverrideWithSynthesizedResponse(nsAutoPtr& aResponseHead, nsInputStreamPump* aPump);
 
   RequestHeaderTuples mClientSetRequestHeaders;
   nsCOMPtr mRedirectChannelChild;
diff --git a/netwerk/protocol/http/InterceptedChannel.cpp b/netwerk/protocol/http/InterceptedChannel.cpp
index 7f5f8e38ad14..9cc4ead8b476 100644
--- a/netwerk/protocol/http/InterceptedChannel.cpp
+++ b/netwerk/protocol/http/InterceptedChannel.cpp
@@ -26,8 +26,10 @@ DoAddCacheEntryHeaders(nsHttpChannel *self,
 
 NS_IMPL_ISUPPORTS(InterceptedChannelBase, nsIInterceptedChannel)
 
-InterceptedChannelBase::InterceptedChannelBase(nsINetworkInterceptController* aController)
+InterceptedChannelBase::InterceptedChannelBase(nsINetworkInterceptController* aController,
+                                               bool aIsNavigation)
 : mController(aController)
+, mIsNavigation(aIsNavigation)
 {
 }
 
@@ -35,21 +37,36 @@ InterceptedChannelBase::~InterceptedChannelBase()
 {
 }
 
+NS_IMETHODIMP
+InterceptedChannelBase::GetResponseBody(nsIOutputStream** aStream)
+{
+  NS_IF_ADDREF(*aStream = mResponseBody);
+  return NS_OK;
+}
+
 void
 InterceptedChannelBase::EnsureSynthesizedResponse()
 {
   if (mSynthesizedResponseHead.isNothing()) {
-    mSynthesizedResponseHead.emplace();
+    mSynthesizedResponseHead.emplace(new nsHttpResponseHead());
   }
 }
 
 void
-InterceptedChannelBase::DoNotifyController(nsIOutputStream* aOut)
+InterceptedChannelBase::DoNotifyController()
 {
-    nsresult rv = mController->ChannelIntercepted(this, aOut);
+    nsresult rv = mController->ChannelIntercepted(this);
+    mController = nullptr;
     NS_ENSURE_SUCCESS_VOID(rv);
 }
 
+NS_IMETHODIMP
+InterceptedChannelBase::GetIsNavigation(bool* aIsNavigation)
+{
+  *aIsNavigation = mIsNavigation;
+  return NS_OK;
+}
+
 nsresult
 InterceptedChannelBase::DoSynthesizeHeader(const nsACString& aName, const nsACString& aValue)
 {
@@ -57,7 +74,7 @@ InterceptedChannelBase::DoSynthesizeHeader(const nsACString& aName, const nsACSt
 
     nsAutoCString header = aName + NS_LITERAL_CSTRING(": ") + aValue;
     // Overwrite any existing header.
-    nsresult rv = mSynthesizedResponseHead->ParseHeaderLine(header.get());
+    nsresult rv = (*mSynthesizedResponseHead)->ParseHeaderLine(header.get());
     NS_ENSURE_SUCCESS(rv, rv);
     return NS_OK;
 }
@@ -65,7 +82,7 @@ InterceptedChannelBase::DoSynthesizeHeader(const nsACString& aName, const nsACSt
 InterceptedChannelChrome::InterceptedChannelChrome(nsHttpChannel* aChannel,
                                                    nsINetworkInterceptController* aController,
                                                    nsICacheEntry* aEntry)
-: InterceptedChannelBase(aController)
+: InterceptedChannelBase(aController, aChannel->IsNavigation())
 , mChannel(aChannel)
 , mSynthesizedCacheEntry(aEntry)
 {
@@ -76,10 +93,17 @@ InterceptedChannelChrome::NotifyController()
 {
   nsCOMPtr out;
 
-  nsresult rv = mSynthesizedCacheEntry->OpenOutputStream(0, getter_AddRefs(out));
+  nsresult rv = mSynthesizedCacheEntry->OpenOutputStream(0, getter_AddRefs(mResponseBody));
   NS_ENSURE_SUCCESS_VOID(rv);
 
-  DoNotifyController(out);
+  DoNotifyController();
+}
+
+NS_IMETHODIMP
+InterceptedChannelChrome::GetChannel(nsIChannel** aChannel)
+{
+  NS_IF_ADDREF(*aChannel = mChannel);
+  return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -132,7 +156,7 @@ InterceptedChannelChrome::FinishSynthesizedResponse()
 
   rv = DoAddCacheEntryHeaders(mChannel, mSynthesizedCacheEntry,
                               mChannel->GetRequestHead(),
-                              mSynthesizedResponseHead.ptr(), securityInfo);
+                              mSynthesizedResponseHead.ref(), securityInfo);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr uri;
@@ -156,10 +180,24 @@ InterceptedChannelChrome::FinishSynthesizedResponse()
   return NS_OK;
 }
 
+NS_IMETHODIMP
+InterceptedChannelChrome::Cancel()
+{
+  if (!mChannel) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // we need to use AsyncAbort instead of Cancel since there's no active pump
+  // to cancel which will provide OnStart/OnStopRequest to the channel.
+  nsresult rv = mChannel->AsyncAbort(NS_BINDING_ABORTED);
+  NS_ENSURE_SUCCESS(rv, rv);
+  return NS_OK;
+}
+
 InterceptedChannelContent::InterceptedChannelContent(HttpChannelChild* aChannel,
                                                      nsINetworkInterceptController* aController,
                                                      nsIStreamListener* aListener)
-: InterceptedChannelBase(aController)
+: InterceptedChannelBase(aController, aChannel->IsNavigation())
 , mChannel(aChannel)
 , mStreamListener(aListener)
 {
@@ -169,11 +207,18 @@ void
 InterceptedChannelContent::NotifyController()
 {
   nsresult rv = NS_NewPipe(getter_AddRefs(mSynthesizedInput),
-                           getter_AddRefs(mSynthesizedOutput),
+                           getter_AddRefs(mResponseBody),
                            0, UINT32_MAX, true, true);
   NS_ENSURE_SUCCESS_VOID(rv);
 
-  DoNotifyController(mSynthesizedOutput);
+  DoNotifyController();
+}
+
+NS_IMETHODIMP
+InterceptedChannelContent::GetChannel(nsIChannel** aChannel)
+{
+  NS_IF_ADDREF(*aChannel = mChannel);
+  return NS_OK;
 }
 
 NS_IMETHODIMP
@@ -183,7 +228,7 @@ InterceptedChannelContent::ResetInterception()
     return NS_ERROR_NOT_AVAILABLE;
   }
 
-  mSynthesizedOutput = nullptr;
+  mResponseBody = nullptr;
   mSynthesizedInput = nullptr;
 
   mChannel->ResetInterception();
@@ -194,7 +239,7 @@ InterceptedChannelContent::ResetInterception()
 NS_IMETHODIMP
 InterceptedChannelContent::SynthesizeHeader(const nsACString& aName, const nsACString& aValue)
 {
-  if (!mSynthesizedOutput) {
+  if (!mResponseBody) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
@@ -204,7 +249,7 @@ InterceptedChannelContent::SynthesizeHeader(const nsACString& aName, const nsACS
 NS_IMETHODIMP
 InterceptedChannelContent::FinishSynthesizedResponse()
 {
-  if (!mChannel) {
+  if (NS_WARN_IF(!mChannel)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
@@ -212,19 +257,36 @@ InterceptedChannelContent::FinishSynthesizedResponse()
 
   nsresult rv = nsInputStreamPump::Create(getter_AddRefs(mStoragePump), mSynthesizedInput,
                                           int64_t(-1), int64_t(-1), 0, 0, true);
-  if (NS_FAILED(rv)) {
+  if (NS_WARN_IF(NS_FAILED(rv))) {
     mSynthesizedInput->Close();
     return rv;
   }
 
-  mSynthesizedOutput = nullptr;
+  mResponseBody = nullptr;
 
   rv = mStoragePump->AsyncRead(mStreamListener, nullptr);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  mChannel->OverrideWithSynthesizedResponse(mSynthesizedResponseHead.ptr(), mStoragePump);
+  mChannel->OverrideWithSynthesizedResponse(mSynthesizedResponseHead.ref(), mStoragePump);
 
   mChannel = nullptr;
+  mStreamListener = nullptr;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptedChannelContent::Cancel()
+{
+  if (!mChannel) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // we need to use AsyncAbort instead of Cancel since there's no active pump
+  // to cancel which will provide OnStart/OnStopRequest to the channel.
+  nsresult rv = mChannel->AsyncAbort(NS_BINDING_ABORTED);
+  NS_ENSURE_SUCCESS(rv, rv);
+  mChannel = nullptr;
+  mStreamListener = nullptr;
   return NS_OK;
 }
 
diff --git a/netwerk/protocol/http/InterceptedChannel.h b/netwerk/protocol/http/InterceptedChannel.h
index 695f5d5befa3..30da2551216a 100644
--- a/netwerk/protocol/http/InterceptedChannel.h
+++ b/netwerk/protocol/http/InterceptedChannel.h
@@ -30,22 +30,32 @@ protected:
   // The interception controller to notify about the successful channel interception
   nsCOMPtr mController;
 
+  // The stream to write the body of the synthesized response
+  nsCOMPtr mResponseBody;
+
   // Response head for use when synthesizing
-  Maybe mSynthesizedResponseHead;
+  Maybe> mSynthesizedResponseHead;
+
+  // Whether this intercepted channel was performing a navigation.
+  bool mIsNavigation;
 
   void EnsureSynthesizedResponse();
-  void DoNotifyController(nsIOutputStream* aOut);
+  void DoNotifyController();
   nsresult DoSynthesizeHeader(const nsACString& aName, const nsACString& aValue);
 
   virtual ~InterceptedChannelBase();
 public:
-  explicit InterceptedChannelBase(nsINetworkInterceptController* aController);
+  InterceptedChannelBase(nsINetworkInterceptController* aController,
+                         bool aIsNavigation);
 
   // Notify the interception controller that the channel has been intercepted
   // and prepare the response body output stream.
   virtual void NotifyController() = 0;
 
   NS_DECL_ISUPPORTS
+
+  NS_IMETHOD GetResponseBody(nsIOutputStream** aOutput) MOZ_OVERRIDE;
+  NS_IMETHOD GetIsNavigation(bool* aIsNavigation) MOZ_OVERRIDE;
 };
 
 class InterceptedChannelChrome : public InterceptedChannelBase
@@ -60,7 +70,11 @@ public:
                            nsINetworkInterceptController* aController,
                            nsICacheEntry* aEntry);
 
-  NS_DECL_NSIINTERCEPTEDCHANNEL
+  NS_IMETHOD ResetInterception() MOZ_OVERRIDE;
+  NS_IMETHOD FinishSynthesizedResponse() MOZ_OVERRIDE;
+  NS_IMETHOD GetChannel(nsIChannel** aChannel) MOZ_OVERRIDE;
+  NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) MOZ_OVERRIDE;
+  NS_IMETHOD Cancel() MOZ_OVERRIDE;
 
   virtual void NotifyController() MOZ_OVERRIDE;
 };
@@ -70,8 +84,7 @@ class InterceptedChannelContent : public InterceptedChannelBase
   // The actual channel being intercepted.
   nsRefPtr mChannel;
 
-  // Writeable buffer for use when synthesizing a response in a child process
-  nsCOMPtr mSynthesizedOutput;
+  // Reader-side of the response body when synthesizing in a child proces
   nsCOMPtr mSynthesizedInput;
 
   // Pump to read the synthesized body in child processes
@@ -85,7 +98,11 @@ public:
                             nsINetworkInterceptController* aController,
                             nsIStreamListener* aListener);
 
-  NS_DECL_NSIINTERCEPTEDCHANNEL
+  NS_IMETHOD ResetInterception() MOZ_OVERRIDE;
+  NS_IMETHOD FinishSynthesizedResponse() MOZ_OVERRIDE;
+  NS_IMETHOD GetChannel(nsIChannel** aChannel) MOZ_OVERRIDE;
+  NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) MOZ_OVERRIDE;
+  NS_IMETHOD Cancel() MOZ_OVERRIDE;
 
   virtual void NotifyController() MOZ_OVERRIDE;
 };
diff --git a/netwerk/test/unit/test_synthesized_response.js b/netwerk/test/unit/test_synthesized_response.js
index 00d489387aa8..47b70f1b8061 100644
--- a/netwerk/test/unit/test_synthesized_response.js
+++ b/netwerk/test/unit/test_synthesized_response.js
@@ -55,19 +55,19 @@ function make_channel(url, body, cb) {
       this.numChecks++;
       return true;
     },
-    channelIntercepted: function(channel, stream) {
+    channelIntercepted: function(channel) {
       channel.QueryInterface(Ci.nsIInterceptedChannel);
       if (body) {
         var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                             .createInstance(Ci.nsIStringInputStream);
         synthesized.data = body;
 
-        NetUtil.asyncCopy(synthesized, stream, function() {
+        NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
           channel.finishSynthesizedResponse();
         });
       }
       if (cb) {
-        cb(channel, stream);
+        cb(channel);
       }
     },
   };
@@ -143,12 +143,12 @@ add_test(function() {
 
 // ensure that the channel waits for a decision and synthesizes headers correctly
 add_test(function() {
-  var chan = make_channel(URL + '/body', null, function(channel, stream) {
+  var chan = make_channel(URL + '/body', null, function(channel) {
     do_timeout(100, function() {
       var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                           .createInstance(Ci.nsIStringInputStream);
       synthesized.data = NON_REMOTE_BODY;
-      NetUtil.asyncCopy(synthesized, stream, function() {
+      NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
         channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
         channel.finishSynthesizedResponse();
       });
@@ -169,12 +169,12 @@ add_test(function() {
 
 // ensure that the intercepted channel supports suspend/resume
 add_test(function() {
-  var chan = make_channel(URL + '/body', null, function(intercepted, stream) {
+  var chan = make_channel(URL + '/body', null, function(intercepted) {
     var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     synthesized.data = NON_REMOTE_BODY;
 
-    NetUtil.asyncCopy(synthesized, stream, function() {
+    NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
       // set the content-type to ensure that the stream converter doesn't hold up notifications
       // and cause the test to fail
       intercepted.synthesizeHeader("Content-Type", "text/plain");
@@ -185,6 +185,65 @@ add_test(function() {
 				     CL_ALLOW_UNKNOWN_CL | CL_SUSPEND | CL_EXPECT_3S_DELAY), null);
 });
 
+// ensure that the intercepted channel can be cancelled
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(intercepted) {
+    intercepted.cancel();
+  });
+  chan.asyncOpen(new ChannelListener(run_next_test, null,
+				     CL_EXPECT_FAILURE), null);
+});
+
+// ensure that the channel can't be cancelled via nsIInterceptedChannel after making a decision
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(chan) {
+    chan.resetInterception();
+    do_timeout(0, function() {
+      var gotexception = false;
+      try {
+        chan.cancel();
+      } catch (x) {
+        gotexception = true;
+      }
+      do_check_true(gotexception);
+    });
+  });
+  chan.asyncOpen(new ChannelListener(handle_remote_response, null), null);
+});
+
+// ensure that the intercepted channel can be canceled during the response
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(intercepted) {
+    var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
+                        .createInstance(Ci.nsIStringInputStream);
+    synthesized.data = NON_REMOTE_BODY;
+
+    NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
+      let channel = intercepted.channel;
+      intercepted.finishSynthesizedResponse();
+      channel.cancel(Cr.NS_BINDING_ABORTED);
+    });
+  });
+  chan.asyncOpen(new ChannelListener(run_next_test, null,
+                                     CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL), null);
+});
+
+// ensure that the intercepted channel can be canceled before the response
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(intercepted) {
+    var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
+                        .createInstance(Ci.nsIStringInputStream);
+    synthesized.data = NON_REMOTE_BODY;
+
+    NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
+      intercepted.channel.cancel(Cr.NS_BINDING_ABORTED);
+      intercepted.finishSynthesizedResponse();
+    });
+  });
+  chan.asyncOpen(new ChannelListener(run_next_test, null,
+                                     CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL), null);
+});
+
 add_test(function() {
   httpServer.stop(run_next_test);
 });