Bug 1225470 Report a message to the console when a service worker waitUntil() is rejected. r=baku
authorBen Kelly <ben@wanderview.com>
Thu, 19 Nov 2015 13:15:17 -0800
changeset 273319 c18e0cc2b208f94b5ee77437049156d928fef334
parent 273318 3c907bb9ba04a024bfd6a4d1f36401ec4f411ce2
child 273320 e9872e15ca295d82c2cf5e0e725c666b4a670c83
push id68260
push userbkelly@mozilla.com
push dateThu, 19 Nov 2015 21:15:23 +0000
treeherdermozilla-inbound@c18e0cc2b208 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku
bugs1225470
milestone45.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 1225470 Report a message to the console when a service worker waitUntil() is rejected. r=baku
dom/bindings/Bindings.conf
dom/workers/ServiceWorkerEvents.cpp
dom/workers/ServiceWorkerEvents.h
dom/workers/ServiceWorkerPrivate.cpp
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -490,16 +490,17 @@ DOMInterfaces = {
     'binaryNames': {
         'message': 'messageMoz',
     },
 },
 
 'ExtendableEvent': {
     'headerFile': 'mozilla/dom/ServiceWorkerEvents.h',
     'nativeType': 'mozilla::dom::workers::ExtendableEvent',
+    'implicitJSContext': [ 'waitUntil' ],
 },
 
 'ExtendableMessageEvent': {
     'headerFile': 'mozilla/dom/ServiceWorkerEvents.h',
     'nativeType': 'mozilla::dom::workers::ExtendableMessageEvent',
 },
 
 'FetchEvent': {
--- a/dom/workers/ServiceWorkerEvents.cpp
+++ b/dom/workers/ServiceWorkerEvents.cpp
@@ -692,17 +692,19 @@ FetchEvent::RespondWith(JSContext* aCx, 
   mWaitToRespond = true;
   RefPtr<RespondWithHandler> handler =
     new RespondWithHandler(mChannel, mRequest->Mode(), ir->IsClientRequest(),
                            ir->IsNavigationRequest(), mScriptSpec,
                            NS_ConvertUTF8toUTF16(requestURL),
                            spec, line, column);
   aArg.AppendNativeHandler(handler);
 
-  WaitUntil(aArg, aRv);
+  // Append directly to the lifecycle promises array.  Don't call WaitUntil()
+  // because that will lead to double-reporting any errors.
+  mPromises.AppendElement(&aArg);
 }
 
 void
 FetchEvent::PreventDefault(JSContext* aCx)
 {
   MOZ_ASSERT(aCx);
 
   if (mPreventDefaultScriptSpec.IsEmpty()) {
@@ -734,39 +736,133 @@ FetchEvent::ReportCanceled()
   //nsString requestURL;
   //CopyUTF8toUTF16(url, requestURL);
 
   ::AsyncLog(mChannel.get(), mPreventDefaultScriptSpec,
              mPreventDefaultLineNumber, mPreventDefaultColumnNumber,
              NS_LITERAL_CSTRING("InterceptionCanceledWithURL"), &requestURL);
 }
 
+namespace {
+
+class WaitUntilHandler final : public PromiseNativeHandler
+{
+  WorkerPrivate* mWorkerPrivate;
+  const nsCString mScope;
+  nsCString mSourceSpec;
+  uint32_t mLine;
+  uint32_t mColumn;
+  nsString mRejectValue;
+
+  ~WaitUntilHandler()
+  {
+  }
+
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+
+  WaitUntilHandler(WorkerPrivate* aWorkerPrivate, JSContext* aCx)
+    : mWorkerPrivate(aWorkerPrivate)
+    , mScope(mWorkerPrivate->WorkerName())
+    , mLine(0)
+    , mColumn(0)
+  {
+    mWorkerPrivate->AssertIsOnWorkerThread();
+
+    // Save the location of the waitUntil() call itself as a fallback
+    // in case the rejection value does not contain any location info.
+    nsJSUtils::GetCallingLocation(aCx, mSourceSpec, &mLine, &mColumn);
+  }
+
+  void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+  {
+    // do nothing, we are only here to report errors
+  }
+
+  void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override
+  {
+    mWorkerPrivate->AssertIsOnWorkerThread();
+
+    nsCString spec;
+    uint32_t line = 0;
+    uint32_t column = 0;
+    ExtractErrorValues(aCx, aValue, spec, &line, &column, mRejectValue);
+
+    // only use the extracted location if we found one
+    if (!spec.IsEmpty()) {
+      mSourceSpec = spec;
+      mLine = line;
+      mColumn = column;
+    }
+
+    nsCOMPtr<nsIRunnable> runnable =
+      NS_NewRunnableMethod(this, &WaitUntilHandler::ReportOnMainThread);
+    MOZ_ALWAYS_TRUE(NS_SUCCEEDED(
+      NS_DispatchToMainThread(runnable.forget())));
+  }
+
+  void
+  ReportOnMainThread()
+  {
+    AssertIsOnMainThread();
+    RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+
+    // TODO: Make the error message a localized string. (bug 1222720)
+    nsString message;
+    message.AppendLiteral("Service worker event waitUntil() was passed a "
+                          "promise that rejected with '");
+    message.Append(mRejectValue);
+    message.AppendLiteral("'.");
+
+    // Note, there is a corner case where this won't report to the window
+    // that triggered the error.  Consider a navigation fetch event that
+    // rejects waitUntil() without holding respondWith() open.  In this case
+    // there is no controlling document yet, the window did call .register()
+    // because there is no documeny yet, and the navigation is no longer
+    // being intercepted.
+
+    swm->ReportToAllClients(mScope, message, NS_ConvertUTF8toUTF16(mSourceSpec),
+                            EmptyString(), mLine, mColumn,
+                            nsIScriptError::errorFlag);
+  }
+};
+
+NS_IMPL_ISUPPORTS0(WaitUntilHandler)
+
+} // anonymous namespace
+
 NS_IMPL_ADDREF_INHERITED(FetchEvent, ExtendableEvent)
 NS_IMPL_RELEASE_INHERITED(FetchEvent, ExtendableEvent)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(FetchEvent)
 NS_INTERFACE_MAP_END_INHERITING(ExtendableEvent)
 
 NS_IMPL_CYCLE_COLLECTION_INHERITED(FetchEvent, ExtendableEvent, mRequest)
 
 ExtendableEvent::ExtendableEvent(EventTarget* aOwner)
   : Event(aOwner, nullptr, nullptr)
 {
 }
 
 void
-ExtendableEvent::WaitUntil(Promise& aPromise, ErrorResult& aRv)
+ExtendableEvent::WaitUntil(JSContext* aCx, Promise& aPromise, ErrorResult& aRv)
 {
   MOZ_ASSERT(!NS_IsMainThread());
 
   if (EventPhase() == nsIDOMEvent::NONE) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
 
+  // Append our handler to each waitUntil promise separately so we
+  // can record the location in script where waitUntil was called.
+  RefPtr<WaitUntilHandler> handler =
+    new WaitUntilHandler(GetCurrentThreadWorkerPrivate(), aCx);
+  aPromise.AppendNativeHandler(handler);
+
   mPromises.AppendElement(&aPromise);
 }
 
 already_AddRefed<Promise>
 ExtendableEvent::GetPromise()
 {
   WorkerPrivate* worker = GetCurrentThreadWorkerPrivate();
   MOZ_ASSERT(worker);
--- a/dom/workers/ServiceWorkerEvents.h
+++ b/dom/workers/ServiceWorkerEvents.h
@@ -46,19 +46,19 @@ public:
   CancelChannelRunnable(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
                         nsresult aStatus);
 
   NS_IMETHOD Run() override;
 };
 
 class ExtendableEvent : public Event
 {
+protected:
   nsTArray<RefPtr<Promise>> mPromises;
 
-protected:
   explicit ExtendableEvent(mozilla::dom::EventTarget* aOwner);
   ~ExtendableEvent() {}
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ExtendableEvent, Event)
   NS_FORWARD_TO_EVENT
 
@@ -85,17 +85,17 @@ public:
               const EventInit& aOptions,
               ErrorResult& aRv)
   {
     nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports());
     return Constructor(target, aType, aOptions);
   }
 
   void
-  WaitUntil(Promise& aPromise, ErrorResult& aRv);
+  WaitUntil(JSContext* aCx, Promise& aPromise, ErrorResult& aRv);
 
   already_AddRefed<Promise>
   GetPromise();
 
   virtual ExtendableEvent* AsExtendableEvent() override
   {
     return this;
   }
--- a/dom/workers/ServiceWorkerPrivate.cpp
+++ b/dom/workers/ServiceWorkerPrivate.cpp
@@ -401,31 +401,20 @@ public:
     workerPrivate->AssertIsOnWorkerThread();
 
     mCallback->SetResult(false);
     nsresult rv = NS_DispatchToMainThread(mCallback);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       NS_RUNTIMEABORT("Failed to dispatch life cycle event handler.");
     }
 
-    JS::Rooted<JSObject*> obj(aCx, workerPrivate->GlobalScope()->GetWrapper());
-    JS::ExposeValueToActiveJS(aValue);
-
-    js::ErrorReport report(aCx);
-    if (NS_WARN_IF(!report.init(aCx, aValue))) {
-      JS_ClearPendingException(aCx);
-      return;
-    }
-
-    RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
-    xpcReport->Init(report.report(), report.message(),
-                    /* aIsChrome = */ false, workerPrivate->WindowID());
-
-    RefPtr<AsyncErrorReporter> aer = new AsyncErrorReporter(xpcReport);
-    NS_DispatchToMainThread(aer);
+    // Note, all WaitUntil() rejections are reported to client consoles
+    // by the WaitUntilHandler in ServiceWorkerEvents.  This ensures that
+    // errors in non-lifecycle events like FetchEvent and PushEvent are
+    // reported properly.
   }
 };
 
 NS_IMPL_ISUPPORTS0(LifecycleEventPromiseHandler)
 
 bool
 LifecycleEventWorkerRunnable::DispatchLifecycleEvent(JSContext* aCx,
                                                      WorkerPrivate* aWorkerPrivate)