Bug 1257096, don't try to report about unusual unhandled rejected Promises, r=bz,waldo
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Wed, 20 Apr 2016 02:29:21 +0300
changeset 331824 49293302ac513b9f91da0780c0383612dbf39744
parent 331823 4236b0313ce3efab36d2f06126c7ce7aaf05dbc6
child 331825 bafad7114902b6f01c7a02db49b09a425dfb25a1
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz, waldo
bugs1257096
milestone48.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 1257096, don't try to report about unusual unhandled rejected Promises, r=bz,waldo
dom/base/DOMException.h
dom/base/ScriptSettings.cpp
dom/promise/Promise.cpp
dom/workers/ScriptLoader.cpp
js/src/js.msg
js/src/jsexn.cpp
js/src/jsfriendapi.h
js/xpconnect/src/nsXPConnect.cpp
js/xpconnect/src/xpcpublic.h
--- a/dom/base/DOMException.h
+++ b/dom/base/DOMException.h
@@ -70,16 +70,28 @@ public:
   nsISupports* GetParentObject() const { return nullptr; }
 
   void GetMessageMoz(nsString& retval);
 
   uint32_t Result() const;
 
   void GetName(nsString& retval);
 
+  virtual void GetErrorMessage(nsAString& aRetVal)
+  {
+    // Since GetName and GetMessageMoz are non-virtual and they deal with
+    // different member variables in Exception vs. DOMException, have a 
+    // virtual method to ensure the right error message creation.
+    nsAutoString name;
+    nsAutoString message;
+    GetName(name);
+    GetMessageMoz(message);
+    CreateErrorMessage(name, message, aRetVal);
+  }
+
   // The XPCOM GetFilename does the right thing.  It might throw, but we want to
   // return an empty filename in that case anyway, instead of throwing.
 
   uint32_t LineNumber(JSContext* aCx) const;
 
   uint32_t ColumnNumber() const;
 
   already_AddRefed<nsIStackFrame> GetLocation() const;
@@ -97,16 +109,33 @@ public:
             nsresult aResult,
             const nsACString& aName,
             nsIStackFrame *aLocation,
             nsISupports *aData);
 
 protected:
   virtual ~Exception();
 
+  void CreateErrorMessage(const nsAString& aName, const nsAString& aMessage,
+                          nsAString& aRetVal)
+  {
+    // Create similar error message as what ErrorReport::init does in jsexn.cpp.
+    if (!aName.IsEmpty() && !aMessage.IsEmpty()) {
+      aRetVal.Assign(aName);
+      aRetVal.AppendLiteral(": ");
+      aRetVal.Append(aMessage);
+    } else if (!aName.IsEmpty()) {
+      aRetVal.Assign(aName);
+    } else if (!aMessage.IsEmpty()) {
+      aRetVal.Assign(aMessage);
+    } else {
+      aRetVal.Truncate();
+    }
+  }
+
   nsCString       mMessage;
   nsresult        mResult;
   nsCString       mName;
   nsCOMPtr<nsIStackFrame> mLocation;
   nsCOMPtr<nsISupports> mData;
   nsString        mFilename;
   int             mLineNumber;
   bool            mInitialized;
@@ -146,16 +175,26 @@ public:
   uint16_t Code() const {
     return mCode;
   }
 
   // Intentionally shadow the nsXPCException version.
   void GetMessageMoz(nsString& retval);
   void GetName(nsString& retval);
 
+  virtual void GetErrorMessage(nsAString& aRetVal) override
+  {
+    // See the comment in Exception::GetErrorMessage.
+    nsAutoString name;
+    nsAutoString message;
+    GetName(name);
+    GetMessageMoz(message);
+    CreateErrorMessage(name, message, aRetVal);
+  }
+
   static already_AddRefed<DOMException>
   Create(nsresult aRv);
 
   static already_AddRefed<DOMException>
   Create(nsresult aRv, const nsACString& aMessage);
 
 protected:
 
--- a/dom/base/ScriptSettings.cpp
+++ b/dom/base/ScriptSettings.cpp
@@ -569,17 +569,18 @@ AutoJSAPI::ReportException()
       errorGlobal = xpc::PrivilegedJunkScope();
     } else {
       errorGlobal = workers::GetCurrentThreadWorkerGlobal();
     }
   }
   JSAutoCompartment ac(cx(), errorGlobal);
   JS::Rooted<JS::Value> exn(cx());
   js::ErrorReport jsReport(cx());
-  if (StealException(&exn) && jsReport.init(cx(), exn)) {
+  if (StealException(&exn) &&
+      jsReport.init(cx(), exn, js::ErrorReport::WithSideEffects)) {
     if (mIsMainThread) {
       RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
 
       RefPtr<nsGlobalWindow> win = xpc::WindowGlobalOrNull(errorGlobal);
       if (!win) {
         // We run addons in a separate privileged compartment, but they still
         // expect to trigger the onerror handler of their associated DOM Window.
         win = xpc::AddonWindowOrNull(errorGlobal);
--- a/dom/promise/Promise.cpp
+++ b/dom/promise/Promise.cpp
@@ -10,16 +10,18 @@
 
 #include "mozilla/Atomics.h"
 #include "mozilla/CycleCollectedJSRuntime.h"
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/Preferences.h"
 
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/DOMError.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/DOMExceptionBinding.h"
 #include "mozilla/dom/MediaStreamError.h"
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/ScriptSettings.h"
 
 #include "jsfriendapi.h"
 #include "js/StructuredClone.h"
 #include "nsContentUtils.h"
 #include "nsGlobalWindow.h"
@@ -2518,27 +2520,40 @@ Promise::MaybeReportRejected()
 
   JSAutoCompartment ac(cx, obj);
   if (!JS_WrapValue(cx, &val)) {
     JS_ClearPendingException(cx);
     return;
   }
 
   js::ErrorReport report(cx);
-  if (!report.init(cx, val)) {
-    JS_ClearPendingException(cx);
-    return;
+  RefPtr<Exception> exp;
+  bool isObject = val.isObject();
+  if (!isObject || NS_FAILED(UNWRAP_OBJECT(Exception, &val.toObject(), exp))) {
+    if (!isObject ||
+        NS_FAILED(UNWRAP_OBJECT(DOMException, &val.toObject(), exp))) {
+      if (!report.init(cx, val, js::ErrorReport::NoSideEffects)) {
+        NS_WARNING("Couldn't convert the unhandled rejected value to an exception.");
+        JS_ClearPendingException(cx);
+        return;
+      }
+    }
   }
 
   RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
   bool isMainThread = MOZ_LIKELY(NS_IsMainThread());
   bool isChrome = isMainThread ? nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(obj))
                                : GetCurrentThreadWorkerPrivate()->IsChromeWorker();
   nsGlobalWindow* win = isMainThread ? xpc::WindowGlobalOrNull(obj) : nullptr;
-  xpcReport->Init(report.report(), report.message(), isChrome, win ? win->AsInner()->WindowID() : 0);
+  uint64_t windowID = win ? win->AsInner()->WindowID() : 0;
+  if (exp) {
+    xpcReport->Init(cx, exp, isChrome, windowID);
+  } else {
+    xpcReport->Init(report.report(), report.message(), isChrome, windowID);
+  }
 
   // Now post an event to do the real reporting async
   // Since Promises preserve their wrapper, it is essential to RefPtr<> the
   // AsyncErrorReporter, otherwise if the call to DispatchToMainThread fails, it
   // will leak. See Bug 958684.  So... don't use DispatchToMainThread()
   nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
   if (NS_WARN_IF(!mainThread)) {
     // Would prefer NS_ASSERTION, but that causes failure in xpcshell tests
--- a/dom/workers/ScriptLoader.cpp
+++ b/dom/workers/ScriptLoader.cpp
@@ -1958,17 +1958,17 @@ ScriptExecutorRunnable::LogExceptionToCo
     return;
   }
 
   // Now the exception state should all be in exn.
   MOZ_ASSERT(!JS_IsExceptionPending(aCx));
   MOZ_ASSERT(!mScriptLoader.mRv.Failed());
 
   js::ErrorReport report(aCx);
-  if (!report.init(aCx, exn)) {
+  if (!report.init(aCx, exn, js::ErrorReport::WithSideEffects)) {
     JS_ClearPendingException(aCx);
     return;
   }
 
   RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport();
   xpcReport->Init(report.report(), report.message(),
                   aWorkerPrivate->IsChromeWorker(), aWorkerPrivate->WindowID());
 
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -161,16 +161,17 @@ MSG_DEF(JSMSG_USER_DEFINED_ERROR,      0
 
 // Internal errors
 MSG_DEF(JSMSG_ALLOC_OVERFLOW,          0, JSEXN_INTERNALERR, "allocation size overflow")
 MSG_DEF(JSMSG_BAD_BUILD_ID,            0, JSEXN_INTERNALERR, "bad build ID for XDR script")
 MSG_DEF(JSMSG_BAD_BYTECODE,            1, JSEXN_INTERNALERR, "unimplemented JavaScript bytecode {0}")
 MSG_DEF(JSMSG_BUFFER_TOO_SMALL,        0, JSEXN_INTERNALERR, "buffer too small")
 MSG_DEF(JSMSG_BUILD_ID_NOT_AVAILABLE,  0, JSEXN_INTERNALERR, "build ID is not available")
 MSG_DEF(JSMSG_BYTECODE_TOO_BIG,        2, JSEXN_INTERNALERR, "bytecode {0} too large (limit {1})")
+MSG_DEF(JSMSG_ERR_DURING_THROW,        0, JSEXN_INTERNALERR, "an internal error occurred while throwing an exception")
 MSG_DEF(JSMSG_NEED_DIET,               1, JSEXN_INTERNALERR, "{0} too large")
 MSG_DEF(JSMSG_OUT_OF_MEMORY,           0, JSEXN_INTERNALERR, "out of memory")
 MSG_DEF(JSMSG_OVER_RECURSED,           0, JSEXN_INTERNALERR, "too much recursion")
 MSG_DEF(JSMSG_TOO_BIG_TO_ENCODE,       0, JSEXN_INTERNALERR, "data are to big to encode")
 MSG_DEF(JSMSG_TOO_DEEP,                1, JSEXN_INTERNALERR, "{0} nested too deeply")
 MSG_DEF(JSMSG_UNCAUGHT_EXCEPTION,      1, JSEXN_INTERNALERR, "uncaught exception: {0}")
 MSG_DEF(JSMSG_UNKNOWN_FORMAT,          1, JSEXN_INTERNALERR, "unknown bytecode format {0}")
 
--- a/js/src/jsexn.cpp
+++ b/js/src/jsexn.cpp
@@ -662,17 +662,17 @@ js::ReportUncaughtException(JSContext* c
     if (!cx->getPendingException(&exn)) {
         cx->clearPendingException();
         return false;
     }
 
     cx->clearPendingException();
 
     ErrorReport err(cx);
-    if (!err.init(cx, exn)) {
+    if (!err.init(cx, exn, js::ErrorReport::WithSideEffects)) {
         cx->clearPendingException();
         return false;
     }
 
     cx->setPendingException(exn);
     CallErrorReporter(cx, err.message(), err.report());
     cx->clearPendingException();
     return true;
@@ -766,26 +766,34 @@ ErrorReport::ReportAddonExceptionToTelem
                 addonIdChars.get(),
                 funname,
                 filename,
                 (reportp ? reportp->lineno : 0) );
     cx->runtime()->addTelemetry(JS_TELEMETRY_ADDON_EXCEPTIONS, 1, histogramKey);
 }
 
 bool
-ErrorReport::init(JSContext* cx, HandleValue exn)
+ErrorReport::init(JSContext* cx, HandleValue exn,
+                  SniffingBehavior sniffingBehavior)
 {
     MOZ_ASSERT(!cx->isExceptionPending());
+    MOZ_ASSERT(!reportp);
 
     if (exn.isObject()) {
         // Because ToString below could error and an exception object could become
         // unrooted, we must root our exception object, if any.
         exnObject = &exn.toObject();
         reportp = ErrorFromException(cx, exnObject);
 
+        if (!reportp && sniffingBehavior == NoSideEffects) {
+            JS_ReportErrorNumber(cx, GetErrorMessage, nullptr,
+                                 JSMSG_ERR_DURING_THROW);
+            return false;
+        }
+
         // Let's see if the exception is from add-on code, if so, it should be reported
         // to telementry.
         ReportAddonExceptionToTelementry(cx);
     }
 
 
     // Be careful not to invoke ToString if we've already successfully extracted
     // an error report, since the exception might be wrapped in a security
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -1358,17 +1358,51 @@ class MOZ_STACK_CLASS AutoStableStringCh
     bool copyAndInflateLatin1Chars(JSContext*, JS::Handle<JSLinearString*> linearString);
 };
 
 struct MOZ_STACK_CLASS JS_FRIEND_API(ErrorReport)
 {
     explicit ErrorReport(JSContext* cx);
     ~ErrorReport();
 
-    bool init(JSContext* cx, JS::HandleValue exn);
+    enum SniffingBehavior {
+        WithSideEffects,
+        NoSideEffects
+    };
+
+    /**
+     * Generate a JSErrorReport from the provided thrown value.
+     *
+     * If the value is a (possibly wrapped) Error object, the JSErrorReport will
+     * be exactly initialized from the Error object's information, without
+     * observable side effects. (The Error object's JSErrorReport is reused, if
+     * it has one.)
+     *
+     * Otherwise various attempts are made to derive JSErrorReport information
+     * from |exn| and from the current execution state.  This process is
+     * *definitely* inconsistent with any standard, and particulars of the
+     * behavior implemented here generally shouldn't be relied upon.
+     *
+     * If the value of |sniffingBehavior| is |WithSideEffects|, some of these
+     * attempts *may* invoke user-configurable behavior when |exn| is an object:
+     * converting |exn| to a string, detecting and getting properties on |exn|,
+     * accessing |exn|'s prototype chain, and others are possible.  Users *must*
+     * tolerate |ErrorReport::init| potentially having arbitrary effects.  Any
+     * exceptions thrown by these operations will be caught and silently
+     * ignored, and "default" values will be substituted into the JSErrorReport.
+     *
+     * But if the value of |sniffingBehavior| is |NoSideEffects|, these attempts
+     * *will not* invoke any observable side effects.  The JSErrorReport will
+     * simply contain fewer, less precise details.
+     *
+     * Unlike some functions involved in error handling, this function adheres
+     * to the usual JSAPI return value error behavior.
+     */
+    bool init(JSContext* cx, JS::HandleValue exn,
+              SniffingBehavior sniffingBehavior);
 
     JSErrorReport* report()
     {
         return reportp;
     }
 
     const char* message()
     {
--- a/js/xpconnect/src/nsXPConnect.cpp
+++ b/js/xpconnect/src/nsXPConnect.cpp
@@ -16,16 +16,17 @@
 #include "nsJSEnvironment.h"
 #include "nsThreadUtils.h"
 #include "nsDOMJSUtils.h"
 
 #include "WrapperFactory.h"
 #include "AccessCheck.h"
 
 #include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/DOMException.h"
 #include "mozilla/dom/Exceptions.h"
 #include "mozilla/dom/Promise.h"
 
 #include "nsDOMMutationObserver.h"
 #include "nsICycleCollectorListener.h"
 #include "mozilla/XPTInterfaceInfoManager.h"
 #include "nsIObjectInputStream.h"
 #include "nsIObjectOutputStream.h"
@@ -200,16 +201,36 @@ xpc::ErrorReport::Init(JSErrorReport* aR
     }
 
     mLineNumber = aReport->lineno;
     mColumn = aReport->column;
     mFlags = aReport->flags;
     mIsMuted = aReport->isMuted;
 }
 
+void
+xpc::ErrorReport::Init(JSContext* aCx, mozilla::dom::Exception* aException,
+                       bool aIsChrome, uint64_t aWindowID)
+{
+    mCategory = aIsChrome ? NS_LITERAL_CSTRING("chrome javascript")
+                          : NS_LITERAL_CSTRING("content javascript");
+    mWindowID = aWindowID;
+
+    aException->GetErrorMessage(mErrorMsg);
+
+    aException->GetFilename(aCx, mFileName);
+    if (mFileName.IsEmpty()) {
+      mFileName.SetIsVoid(true);
+    }
+    aException->GetLineNumber(aCx, &mLineNumber);
+    aException->GetColumnNumber(&mColumn);
+
+    mFlags = JSREPORT_EXCEPTION;
+}
+
 static LazyLogModule gJSDiagnostics("JSDiagnostics");
 
 void
 xpc::ErrorReport::LogToConsole()
 {
   LogToConsoleWithStack(nullptr);
 }
 void
--- a/js/xpconnect/src/xpcpublic.h
+++ b/js/xpconnect/src/xpcpublic.h
@@ -25,16 +25,22 @@
 #include "nsStringBuffer.h"
 #include "mozilla/dom/BindingDeclarations.h"
 
 class nsGlobalWindow;
 class nsIPrincipal;
 class nsScriptNameSpaceManager;
 class nsIMemoryReporterCallback;
 
+namespace mozilla {
+namespace dom {
+class Exception;
+}
+}
+
 typedef void (* xpcGCCallback)(JSGCStatus status);
 
 namespace xpc {
 
 class Scriptability {
 public:
     explicit Scriptability(JSCompartment* c);
     bool Allowed();
@@ -498,16 +504,18 @@ class ErrorReport {
                   , mLineNumber(0)
                   , mColumn(0)
                   , mFlags(0)
                   , mIsMuted(false)
     {}
 
     void Init(JSErrorReport* aReport, const char* aFallbackMessage,
               bool aIsChrome, uint64_t aWindowID);
+    void Init(JSContext* aCx, mozilla::dom::Exception* aException,
+              bool aIsChrome, uint64_t aWindowID);
     // Log the error report to the console.  Which console will depend on the
     // window id it was initialized with.
     void LogToConsole();
     // Log to console, using the given stack object (which should be a stack of
     // the sort that JS::CaptureCurrentStack produces).  aStack is allowed to be
     // null.
     void LogToConsoleWithStack(JS::HandleObject aStack);