Bug 1181127 - Don't run service workers for fetch events if no fetch handlers were added during script's evaluation. r=bkelly
authorCatalin Badea <catalin.badea392@gmail.com>
Mon, 19 Dec 2016 04:38:53 +0200
changeset 371404 e87c846f980997a04eeeedf22a6c470e84af08d2
parent 371349 12f4a1387fbc20a4696cd0cc9ea1ae7cddeb213c
child 371405 1ccd58a14a6a7d3aed4653e9e24e737f0b25a8ad
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbkelly
bugs1181127
milestone53.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 1181127 - Don't run service workers for fetch events if no fetch handlers were added during script's evaluation. r=bkelly Display a warning when event listeners are added after the script's evaluation.
dom/locales/en-US/chrome/dom/dom.properties
dom/workers/ServiceWorkerInfo.cpp
dom/workers/ServiceWorkerInfo.h
dom/workers/ServiceWorkerManager.cpp
dom/workers/ServiceWorkerPrivate.cpp
dom/workers/ServiceWorkerPrivate.h
dom/workers/ServiceWorkerRegistrar.cpp
dom/workers/ServiceWorkerRegistrar.h
dom/workers/ServiceWorkerRegistrarTypes.ipdlh
dom/workers/WorkerPrivate.cpp
dom/workers/WorkerPrivate.h
dom/workers/WorkerScope.cpp
dom/workers/WorkerScope.h
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -224,16 +224,18 @@ PrefixedImageSmoothingEnabledWarning=Use
 # LOCALIZATION NOTE: Do not translate "ServiceWorker", "Service-Worker-Allowed" or "HTTP". %1$S and %2$S are URLs.
 ServiceWorkerScopePathMismatch=Failed to register a ServiceWorker: The path of the provided scope ‘%1$S’ is not under the max scope allowed ‘%2$S’. Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.
 # LOCALIZATION NOTE: Do not translate "ServiceWorker". %1$S is a URL representing the scope of the ServiceWorker, %2$S is a stringified numeric HTTP status code like "404" and %3$S is a URL.
 ServiceWorkerRegisterNetworkError=Failed to register/update a ServiceWorker for scope ‘%1$S’: Load failed with status %2$S for script ‘%3$S’.
 # LOCALIZATION NOTE: Do not translate "ServiceWorker". %1$S is a URL representing the scope of the ServiceWorker, %2$S is a MIME Media Type like "text/plain" and %3$S is a URL.
 ServiceWorkerRegisterMimeTypeError=Failed to register/update a ServiceWorker for scope ‘%1$S’: Bad Content-Type of ‘%2$S’ received for script ‘%3$S’.  Must be ‘text/javascript’, ‘application/x-javascript’, or ‘application/javascript’.
 # LOCALIZATION NOTE: Do not translate "ServiceWorker". %1$S is a URL representing the scope of the ServiceWorker.
 ServiceWorkerGraceTimeoutTermination=Terminating ServiceWorker for scope ‘%1$S’ with pending waitUntil/respondWith promises because of grace timeout.
+# LOCALIZATION NOTE (ServiceWorkerNoFetchHandler): Do not translate "Fetch".
+ServiceWorkerNoFetchHandler=Fetch event handlers must be added during the worker script’s initial evaluation.
 ExecCommandCutCopyDeniedNotInputDriven=document.execCommand(‘cut’/‘copy’) was denied because it was not called from inside a short running user-generated event handler.
 ManifestShouldBeObject=Manifest should be an object.
 ManifestScopeURLInvalid=The scope URL is invalid.
 ManifestScopeNotSameOrigin=The scope URL must be same origin as document.
 ManifestStartURLOutsideScope=The start URL is outside the scope, so the scope is invalid.
 ManifestStartURLInvalid=The start URL is invalid.
 ManifestStartURLShouldBeSameOrigin=The start URL must be same origin as document.
 # LOCALIZATION NOTE: %1$S is the name of the object whose property is invalid. %2$S is the name of the invalid property. %3$S is the expected type of the property value. E.g. "Expected the manifest's start_url member to be a string."
--- a/dom/workers/ServiceWorkerInfo.cpp
+++ b/dom/workers/ServiceWorkerInfo.cpp
@@ -175,16 +175,17 @@ ServiceWorkerInfo::ServiceWorkerInfo(nsI
   : mPrincipal(aPrincipal)
   , mScope(aScope)
   , mScriptSpec(aScriptSpec)
   , mCacheName(aCacheName)
   , mState(ServiceWorkerState::EndGuard_)
   , mServiceWorkerID(GetNextID())
   , mServiceWorkerPrivate(new ServiceWorkerPrivate(this))
   , mSkipWaitingFlag(false)
+  , mHandlesFetch(Unknown)
 {
   MOZ_ASSERT(mPrincipal);
   // cache origin attributes so we can use them off main thread
   mOriginAttributes = BasePrincipal::Cast(mPrincipal)->OriginAttributesRef();
   MOZ_ASSERT(!mScope.IsEmpty());
   MOZ_ASSERT(!mScriptSpec.IsEmpty());
   MOZ_ASSERT(!mCacheName.IsEmpty());
 }
--- a/dom/workers/ServiceWorkerInfo.h
+++ b/dom/workers/ServiceWorkerInfo.h
@@ -41,16 +41,22 @@ private:
   // addition and removal.
   // There is a high chance of there being at least one ServiceWorker
   // associated with this all the time.
   AutoTArray<ServiceWorker*, 1> mInstances;
 
   RefPtr<ServiceWorkerPrivate> mServiceWorkerPrivate;
   bool mSkipWaitingFlag;
 
+  enum {
+    Unknown,
+    Enabled,
+    Disabled
+  } mHandlesFetch;
+
   ~ServiceWorkerInfo();
 
   // Generates a unique id for the service worker, with zero being treated as
   // invalid.
   uint64_t
   GetNextID() const;
 
 public:
@@ -130,16 +136,32 @@ public:
   void
   SetActivateStateUncheckedWithoutEvent(ServiceWorkerState aState)
   {
     AssertIsOnMainThread();
     mState = aState;
   }
 
   void
+  SetHandlesFetch(bool aHandlesFetch)
+  {
+    AssertIsOnMainThread();
+    MOZ_DIAGNOSTIC_ASSERT(mHandlesFetch == Unknown);
+    mHandlesFetch = aHandlesFetch ? Enabled : Disabled;
+  }
+
+  bool
+  HandlesFetch() const
+  {
+    AssertIsOnMainThread();
+    MOZ_DIAGNOSTIC_ASSERT(mHandlesFetch != Unknown);
+    return mHandlesFetch != Disabled;
+  }
+
+  void
   AppendWorker(ServiceWorker* aWorker);
 
   void
   RemoveWorker(ServiceWorker* aWorker);
 
   already_AddRefed<ServiceWorker>
   GetOrCreateInstance(nsPIDOMWindowInner* aWindow);
 };
--- a/dom/workers/ServiceWorkerManager.cpp
+++ b/dom/workers/ServiceWorkerManager.cpp
@@ -179,16 +179,17 @@ PopulateRegistrationData(nsIPrincipal* a
   RefPtr<ServiceWorkerInfo> newest = aRegistration->Newest();
   if (NS_WARN_IF(!newest)) {
     return NS_ERROR_FAILURE;
   }
 
   if (aRegistration->GetActive()) {
     aData.currentWorkerURL() = aRegistration->GetActive()->ScriptSpec();
     aData.cacheName() = aRegistration->GetActive()->CacheName();
+    aData.currentWorkerHandlesFetch() = aRegistration->GetActive()->HandlesFetch();
   }
 
   return NS_OK;
 }
 
 class TeardownRunnable final : public Runnable
 {
 public:
@@ -1692,16 +1693,18 @@ ServiceWorkerManager::LoadRegistration(
     }
   }
 
   const nsCString& currentWorkerURL = aRegistration.currentWorkerURL();
   if (!currentWorkerURL.IsEmpty()) {
     registration->SetActive(
       new ServiceWorkerInfo(registration->mPrincipal, registration->mScope,
                             currentWorkerURL, aRegistration.cacheName()));
+
+    registration->GetActive()->SetHandlesFetch(aRegistration.currentWorkerHandlesFetch());
     registration->GetActive()->SetActivateStateUncheckedWithoutEvent(ServiceWorkerState::Activated);
   }
 }
 
 void
 ServiceWorkerManager::LoadRegistrations(
                   const nsTArray<ServiceWorkerRegistrationData>& aRegistrations)
 {
--- a/dom/workers/ServiceWorkerPrivate.cpp
+++ b/dom/workers/ServiceWorkerPrivate.cpp
@@ -102,81 +102,101 @@ ServiceWorkerPrivate::~ServiceWorkerPriv
 
   mIdleWorkerTimer->Cancel();
 }
 
 namespace {
 
 class CheckScriptEvaluationWithCallback final : public WorkerRunnable
 {
+  nsMainThreadPtrHandle<ServiceWorkerPrivate> mServiceWorkerPrivate;
   nsMainThreadPtrHandle<KeepAliveToken> mKeepAliveToken;
-  RefPtr<LifeCycleEventCallback> mCallback;
+
+  // The script evaluation result must be reported even if the runnable
+  // is cancelled.
+  RefPtr<LifeCycleEventCallback> mScriptEvaluationCallback;
+
 #ifdef DEBUG
   bool mDone;
 #endif
 
 public:
   CheckScriptEvaluationWithCallback(WorkerPrivate* aWorkerPrivate,
+                                    ServiceWorkerPrivate* aServiceWorkerPrivate,
                                     KeepAliveToken* aKeepAliveToken,
-                                    LifeCycleEventCallback* aCallback)
+                                    LifeCycleEventCallback* aScriptEvaluationCallback)
     : WorkerRunnable(aWorkerPrivate)
+    , mServiceWorkerPrivate(new nsMainThreadPtrHolder<ServiceWorkerPrivate>(aServiceWorkerPrivate))
     , mKeepAliveToken(new nsMainThreadPtrHolder<KeepAliveToken>(aKeepAliveToken))
-    , mCallback(aCallback)
+    , mScriptEvaluationCallback(aScriptEvaluationCallback)
 #ifdef DEBUG
     , mDone(false)
 #endif
   {
     AssertIsOnMainThread();
   }
 
   ~CheckScriptEvaluationWithCallback()
   {
     MOZ_ASSERT(mDone);
   }
 
   bool
   WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override
   {
     aWorkerPrivate->AssertIsOnWorkerThread();
-    Done(aWorkerPrivate->WorkerScriptExecutedSuccessfully());
+
+    bool fetchHandlerWasAdded = aWorkerPrivate->FetchHandlerWasAdded();
+    nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<bool>(this,
+      &CheckScriptEvaluationWithCallback::ReportFetchFlag, fetchHandlerWasAdded);
+    aWorkerPrivate->DispatchToMainThread(runnable.forget());
+
+    ReportScriptEvaluationResult(aWorkerPrivate->WorkerScriptExecutedSuccessfully());
 
     return true;
   }
 
+  void
+  ReportFetchFlag(bool aFetchHandlerWasAdded)
+  {
+    AssertIsOnMainThread();
+    mServiceWorkerPrivate->SetHandlesFetch(aFetchHandlerWasAdded);
+  }
+
   nsresult
   Cancel() override
   {
-    Done(false);
+    ReportScriptEvaluationResult(false);
     return WorkerRunnable::Cancel();
   }
 
 private:
   void
-  Done(bool aResult)
+  ReportScriptEvaluationResult(bool aScriptEvaluationResult)
   {
 #ifdef DEBUG
     mDone = true;
 #endif
-    mCallback->SetResult(aResult);
-    MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(mCallback));
+    mScriptEvaluationCallback->SetResult(aScriptEvaluationResult);
+    MOZ_ALWAYS_SUCCEEDS(mWorkerPrivate->DispatchToMainThread(mScriptEvaluationCallback));
   }
 };
 
 } // anonymous namespace
 
 nsresult
-ServiceWorkerPrivate::CheckScriptEvaluation(LifeCycleEventCallback* aCallback)
+ServiceWorkerPrivate::CheckScriptEvaluation(LifeCycleEventCallback* aScriptEvaluationCallback)
 {
   nsresult rv = SpawnWorkerIfNeeded(LifeCycleEvent, nullptr);
   NS_ENSURE_SUCCESS(rv, rv);
 
   RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken();
   RefPtr<WorkerRunnable> r = new CheckScriptEvaluationWithCallback(mWorkerPrivate,
-                                                                   token,
-                                                                   aCallback);
+                                                                   this, token,
+                                                                   aScriptEvaluationCallback);
   if (NS_WARN_IF(!r->Dispatch())) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
 namespace {
@@ -1677,37 +1697,49 @@ NS_IMPL_ISUPPORTS_INHERITED(FetchEventRu
 nsresult
 ServiceWorkerPrivate::SendFetchEvent(nsIInterceptedChannel* aChannel,
                                      nsILoadGroup* aLoadGroup,
                                      const nsAString& aDocumentId,
                                      bool aIsReload)
 {
   AssertIsOnMainThread();
 
+  if (NS_WARN_IF(!mInfo)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+  MOZ_ASSERT(swm);
+
+  RefPtr<ServiceWorkerRegistrationInfo> registration =
+    swm->GetRegistration(mInfo->GetPrincipal(), mInfo->Scope());
+
+  // Handle Fetch algorithm - step 16. If the service worker didn't register
+  // any fetch event handlers, then abort the interception and maybe trigger
+  // the soft update algorithm.
+  if (!mInfo->HandlesFetch()) {
+    aChannel->ResetInterception();
+
+    // Trigger soft updates if necessary.
+    registration->MaybeScheduleTimeCheckAndUpdate();
+
+    return NS_OK;
+  }
+
   // if the ServiceWorker script fails to load for some reason, just resume
   // the original channel.
   nsCOMPtr<nsIRunnable> failRunnable =
     NewRunnableMethod(aChannel, &nsIInterceptedChannel::ResetInterception);
 
   nsresult rv = SpawnWorkerIfNeeded(FetchEvent, failRunnable, aLoadGroup);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsMainThreadPtrHandle<nsIInterceptedChannel> handle(
     new nsMainThreadPtrHolder<nsIInterceptedChannel>(aChannel, false));
 
-  if (NS_WARN_IF(!mInfo)) {
-    return NS_ERROR_FAILURE;
-  }
-
-  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
-  MOZ_ASSERT(swm);
-
-  RefPtr<ServiceWorkerRegistrationInfo> registration =
-    swm->GetRegistration(mInfo->GetPrincipal(), mInfo->Scope());
-
   nsMainThreadPtrHandle<ServiceWorkerRegistrationInfo> regInfo(
     new nsMainThreadPtrHolder<ServiceWorkerRegistrationInfo>(registration, false));
 
   RefPtr<KeepAliveToken> token = CreateEventKeepAliveToken();
 
   RefPtr<FetchEventRunnable> r =
     new FetchEventRunnable(mWorkerPrivate, token, handle,
                            mInfo->ScriptSpec(), regInfo,
@@ -2115,9 +2147,21 @@ ServiceWorkerPrivate::Observe(nsISupport
     RefPtr<Runnable> runnable = pendingWindows[i];
     MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable));
     pendingWindows.RemoveElementAt(i);
   }
 
   return NS_OK;
 }
 
+void
+ServiceWorkerPrivate::SetHandlesFetch(bool aValue)
+{
+  AssertIsOnMainThread();
+
+  if (NS_WARN_IF(!mInfo)) {
+    return;
+  }
+
+  mInfo->SetHandlesFetch(aValue);
+}
+
 END_WORKERS_NAMESPACE
--- a/dom/workers/ServiceWorkerPrivate.h
+++ b/dom/workers/ServiceWorkerPrivate.h
@@ -148,16 +148,19 @@ public:
   DetachDebugger();
 
   bool
   IsIdle() const;
 
   void
   AddPendingWindow(Runnable* aPendingWindow);
 
+  void
+  SetHandlesFetch(bool aValue);
+
 private:
   enum WakeUpReason {
     FetchEvent = 0,
     PushEvent,
     PushSubscriptionChangeEvent,
     MessageEvent,
     NotificationClickEvent,
     NotificationCloseEvent,
--- a/dom/workers/ServiceWorkerRegistrar.cpp
+++ b/dom/workers/ServiceWorkerRegistrar.cpp
@@ -36,16 +36,17 @@ using namespace mozilla::ipc;
 
 namespace mozilla {
 namespace dom {
 
 namespace {
 
 static const char* gSupportedRegistrarVersions[] = {
   SERVICEWORKERREGISTRAR_VERSION,
+  "4",
   "3",
   "2"
 };
 
 StaticRefPtr<ServiceWorkerRegistrar> gServiceWorkerRegistrar;
 
 } // namespace
 
@@ -346,16 +347,50 @@ ServiceWorkerRegistrar::ReadData()
 
       GET_LINE(entry->scope());
 
       entry->principal() =
         mozilla::ipc::ContentPrincipalInfo(attrs, entry->scope());
 
       GET_LINE(entry->currentWorkerURL());
 
+      nsAutoCString fetchFlag;
+      GET_LINE(fetchFlag);
+      if (!fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE) &&
+          !fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_FALSE)) {
+        return NS_ERROR_INVALID_ARG;
+      }
+      entry->currentWorkerHandlesFetch() =
+        fetchFlag.EqualsLiteral(SERVICEWORKERREGISTRAR_TRUE);
+
+      nsAutoCString cacheName;
+      GET_LINE(cacheName);
+      CopyUTF8toUTF16(cacheName, entry->cacheName());
+    } else if (version.EqualsLiteral("4")) {
+      overwrite = true;
+      dedupe = true;
+
+      nsAutoCString suffix;
+      GET_LINE(suffix);
+
+      PrincipalOriginAttributes attrs;
+      if (!attrs.PopulateFromSuffix(suffix)) {
+        return NS_ERROR_INVALID_ARG;
+      }
+
+      GET_LINE(entry->scope());
+
+      entry->principal() =
+        mozilla::ipc::ContentPrincipalInfo(attrs, entry->scope());
+
+      GET_LINE(entry->currentWorkerURL());
+
+      // default handlesFetch flag to Enabled
+      entry->currentWorkerHandlesFetch() = true;
+
       nsAutoCString cacheName;
       GET_LINE(cacheName);
       CopyUTF8toUTF16(cacheName, entry->cacheName());
     } else if (version.EqualsLiteral("3")) {
       overwrite = true;
       dedupe = true;
 
       nsAutoCString suffix;
@@ -371,16 +406,19 @@ ServiceWorkerRegistrar::ReadData()
 
       GET_LINE(entry->scope());
 
       entry->principal() =
         mozilla::ipc::ContentPrincipalInfo(attrs, entry->scope());
 
       GET_LINE(entry->currentWorkerURL());
 
+      // default handlesFetch flag to Enabled
+      entry->currentWorkerHandlesFetch() = true;
+
       nsAutoCString cacheName;
       GET_LINE(cacheName);
       CopyUTF8toUTF16(cacheName, entry->cacheName());
     } else if (version.EqualsLiteral("2")) {
       overwrite = true;
       dedupe = true;
 
       nsAutoCString suffix;
@@ -399,16 +437,19 @@ ServiceWorkerRegistrar::ReadData()
       entry->principal() =
         mozilla::ipc::ContentPrincipalInfo(attrs, entry->scope());
 
       // scriptSpec is no more used in latest version.
       GET_LINE(unused);
 
       GET_LINE(entry->currentWorkerURL());
 
+      // default handlesFetch flag to Enabled
+      entry->currentWorkerHandlesFetch() = true;
+
       nsAutoCString cacheName;
       GET_LINE(cacheName);
       CopyUTF8toUTF16(cacheName, entry->cacheName());
 
       // waitingCacheName is no more used in latest version.
       GET_LINE(unused);
     } else {
       MOZ_ASSERT_UNREACHABLE("Should never get here!");
@@ -694,16 +735,20 @@ ServiceWorkerRegistrar::WriteData()
     buffer.Append('\n');
 
     buffer.Append(data[i].scope());
     buffer.Append('\n');
 
     buffer.Append(data[i].currentWorkerURL());
     buffer.Append('\n');
 
+    buffer.Append(data[i].currentWorkerHandlesFetch() ?
+                    SERVICEWORKERREGISTRAR_TRUE : SERVICEWORKERREGISTRAR_FALSE);
+    buffer.Append('\n');
+
     buffer.Append(NS_ConvertUTF16toUTF8(data[i].cacheName()));
     buffer.Append('\n');
 
     buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR);
     buffer.Append('\n');
 
     rv = stream->Write(buffer.Data(), buffer.Length(), &count);
     if (NS_WARN_IF(NS_FAILED(rv))) {
--- a/dom/workers/ServiceWorkerRegistrar.h
+++ b/dom/workers/ServiceWorkerRegistrar.h
@@ -11,17 +11,17 @@
 #include "mozilla/Telemetry.h"
 #include "nsClassHashtable.h"
 #include "nsIObserver.h"
 #include "nsCOMPtr.h"
 #include "nsString.h"
 #include "nsTArray.h"
 
 #define SERVICEWORKERREGISTRAR_FILE "serviceworker.txt"
-#define SERVICEWORKERREGISTRAR_VERSION "4"
+#define SERVICEWORKERREGISTRAR_VERSION "5"
 #define SERVICEWORKERREGISTRAR_TERMINATOR "#"
 #define SERVICEWORKERREGISTRAR_TRUE "true"
 #define SERVICEWORKERREGISTRAR_FALSE "false"
 
 class nsIFile;
 
 namespace mozilla {
 
--- a/dom/workers/ServiceWorkerRegistrarTypes.ipdlh
+++ b/dom/workers/ServiceWorkerRegistrarTypes.ipdlh
@@ -8,16 +8,17 @@ include PBackgroundSharedTypes;
 
 namespace mozilla {
 namespace dom {
 
 struct ServiceWorkerRegistrationData
 {
   nsCString scope;
   nsCString currentWorkerURL;
+  bool currentWorkerHandlesFetch;
 
   nsString cacheName;
 
   PrincipalInfo principal;
 };
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -4014,16 +4014,17 @@ WorkerPrivate::WorkerPrivate(WorkerPriva
   , mFrozen(false)
   , mTimerRunning(false)
   , mRunningExpiredTimeouts(false)
   , mPendingEventQueueClearing(false)
   , mCancelAllPendingRunnables(false)
   , mPeriodicGCTimerRunning(false)
   , mIdleGCTimerRunning(false)
   , mWorkerScriptExecutedSuccessfully(false)
+  , mFetchHandlerWasAdded(false)
   , mOnLine(false)
 {
   MOZ_ASSERT_IF(!IsDedicatedWorker(), !aWorkerName.IsVoid());
   MOZ_ASSERT_IF(IsDedicatedWorker(), aWorkerName.IsEmpty());
 
   if (aParent) {
     aParent->AssertIsOnWorkerThread();
     aParent->GetAllPreferences(mPreferences);
--- a/dom/workers/WorkerPrivate.h
+++ b/dom/workers/WorkerPrivate.h
@@ -968,16 +968,17 @@ class WorkerPrivate : public WorkerPriva
   bool mFrozen;
   bool mTimerRunning;
   bool mRunningExpiredTimeouts;
   bool mPendingEventQueueClearing;
   bool mCancelAllPendingRunnables;
   bool mPeriodicGCTimerRunning;
   bool mIdleGCTimerRunning;
   bool mWorkerScriptExecutedSuccessfully;
+  bool mFetchHandlerWasAdded;
   bool mPreferences[WORKERPREF_COUNT];
   bool mOnLine;
 
 protected:
   ~WorkerPrivate();
 
 public:
   static already_AddRefed<WorkerPrivate>
@@ -1206,16 +1207,32 @@ public:
   CycleCollectInternal(bool aCollectChildren);
 
   void
   OfflineStatusChangeEventInternal(bool aIsOffline);
 
   void
   MemoryPressureInternal();
 
+  void
+  SetFetchHandlerWasAdded()
+  {
+    MOZ_ASSERT(IsServiceWorker());
+    AssertIsOnWorkerThread();
+    mFetchHandlerWasAdded = true;
+  }
+
+  bool
+  FetchHandlerWasAdded() const
+  {
+    MOZ_ASSERT(IsServiceWorker());
+    AssertIsOnWorkerThread();
+    return mFetchHandlerWasAdded;
+  }
+
   JSContext*
   GetJSContext() const
   {
     AssertIsOnWorkerThread();
     return mJSContext;
   }
 
   WorkerGlobalScope*
--- a/dom/workers/WorkerScope.cpp
+++ b/dom/workers/WorkerScope.cpp
@@ -606,16 +606,99 @@ ServiceWorkerGlobalScope::Registration()
   if (!mRegistration) {
     mRegistration =
       ServiceWorkerRegistration::CreateForWorker(mWorkerPrivate, mScope);
   }
 
   return mRegistration;
 }
 
+EventHandlerNonNull*
+ServiceWorkerGlobalScope::GetOnfetch()
+{
+  MOZ_ASSERT(mWorkerPrivate);
+  mWorkerPrivate->AssertIsOnWorkerThread();
+
+  return GetEventHandler(nullptr, NS_LITERAL_STRING("fetch"));
+}
+
+namespace {
+
+class ReportFetchListenerWarningRunnable final : public Runnable
+{
+  const nsCString mScope;
+  nsCString mSourceSpec;
+  uint32_t mLine;
+  uint32_t mColumn;
+
+public:
+  explicit ReportFetchListenerWarningRunnable(const nsString& aScope)
+    : mScope(NS_ConvertUTF16toUTF8(aScope))
+  {
+    WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+    MOZ_ASSERT(workerPrivate);
+    JSContext* cx = workerPrivate->GetJSContext();
+    MOZ_ASSERT(cx);
+
+    nsJSUtils::GetCallingLocation(cx, mSourceSpec, &mLine, &mColumn);
+  }
+
+  NS_IMETHOD
+  Run() override
+  {
+    AssertIsOnMainThread();
+
+    ServiceWorkerManager::LocalizeAndReportToAllClients(mScope, "ServiceWorkerNoFetchHandler",
+        nsTArray<nsString>{}, nsIScriptError::warningFlag, NS_ConvertUTF8toUTF16(mSourceSpec),
+        EmptyString(), mLine, mColumn);
+
+    return NS_OK;
+  }
+};
+
+} // anonymous namespace
+
+void
+ServiceWorkerGlobalScope::SetOnfetch(mozilla::dom::EventHandlerNonNull* aCallback)
+{
+  MOZ_ASSERT(mWorkerPrivate);
+  mWorkerPrivate->AssertIsOnWorkerThread();
+
+  if (aCallback) {
+    if (mWorkerPrivate->WorkerScriptExecutedSuccessfully()) {
+      RefPtr<Runnable> r = new ReportFetchListenerWarningRunnable(mScope);
+      mWorkerPrivate->DispatchToMainThread(r.forget());
+    }
+    mWorkerPrivate->SetFetchHandlerWasAdded();
+  }
+  SetEventHandler(nullptr, NS_LITERAL_STRING("fetch"), aCallback);
+}
+
+void
+ServiceWorkerGlobalScope::AddEventListener(
+                          const nsAString& aType,
+                          dom::EventListener* aListener,
+                          const dom::AddEventListenerOptionsOrBoolean& aOptions,
+                          const dom::Nullable<bool>& aWantsUntrusted,
+                          ErrorResult& aRv)
+{
+  MOZ_ASSERT(mWorkerPrivate);
+  mWorkerPrivate->AssertIsOnWorkerThread();
+
+  if (mWorkerPrivate->WorkerScriptExecutedSuccessfully()) {
+    RefPtr<Runnable> r = new ReportFetchListenerWarningRunnable(mScope);
+    mWorkerPrivate->DispatchToMainThread(r.forget());
+  }
+  DOMEventTargetHelper::AddEventListener(aType, aListener, aOptions,
+                                         aWantsUntrusted, aRv);
+  if (!aRv.Failed()) {
+    mWorkerPrivate->SetFetchHandlerWasAdded();
+  }
+}
+
 namespace {
 
 class SkipWaitingResultRunnable final : public WorkerRunnable
 {
   RefPtr<PromiseWorkerProxy> mPromiseProxy;
 
 public:
   SkipWaitingResultRunnable(WorkerPrivate* aWorkerPrivate,
--- a/dom/workers/WorkerScope.h
+++ b/dom/workers/WorkerScope.h
@@ -276,23 +276,35 @@ public:
 
   ServiceWorkerRegistration*
   Registration();
 
   already_AddRefed<Promise>
   SkipWaiting(ErrorResult& aRv);
 
   IMPL_EVENT_HANDLER(activate)
-  IMPL_EVENT_HANDLER(fetch)
   IMPL_EVENT_HANDLER(install)
   IMPL_EVENT_HANDLER(message)
 
   IMPL_EVENT_HANDLER(push)
   IMPL_EVENT_HANDLER(pushsubscriptionchange)
 
+  EventHandlerNonNull*
+  GetOnfetch();
+
+  void
+  SetOnfetch(mozilla::dom::EventHandlerNonNull* aCallback);
+
+  using DOMEventTargetHelper::AddEventListener;
+  virtual void
+  AddEventListener(const nsAString& aType,
+                   dom::EventListener* aListener,
+                   const dom::AddEventListenerOptionsOrBoolean& aOptions,
+                   const dom::Nullable<bool>& aWantsUntrusted,
+                   ErrorResult& aRv) override;
 };
 
 class WorkerDebuggerGlobalScope final : public DOMEventTargetHelper,
                                         public nsIGlobalObject
 {
   typedef mozilla::dom::workers::WorkerPrivate WorkerPrivate;
 
   WorkerPrivate* mWorkerPrivate;