[sourcemaps] Report error message on resourceLoad error

This CL adds reporting of a human readable string which includes the
internal error code if a source map load fails. We could think about
providing a web.dev page with an explanation of the internal error
codes. In any case, this fine-grained distinction should be an
improvement for referencing errors on sites like stack-overflow. If
all else fails, the internal error code can be looked up in
net_error_list.h in the chromium sources.

Screenshot: https://imgur.com/a/dWjG1GS

Bug: chromium:1030746
Change-Id: I04ddbc3f98ab2d6c54fb067a89408cbb60217477
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1998761
Reviewed-by: Peter Marshall <[email protected]>
Reviewed-by: Tim van der Lippe <[email protected]>
Commit-Queue: Sigurd Schneider <[email protected]>
diff --git a/front_end/Tests.js b/front_end/Tests.js
index c9e7139..cc74a7b 100644
--- a/front_end/Tests.js
+++ b/front_end/Tests.js
@@ -1397,8 +1397,8 @@
       return new Promise(fulfill => {
         Host.ResourceLoader.load(url, headers, callback);
 
-        function callback(statusCode, headers, content) {
-          test.assertEquals(expectedStatus, statusCode);
+        function callback(success, headers, content, errorDescription) {
+          test.assertEquals(expectedStatus, errorDescription.statusCode);
 
           const headersArray = [];
           for (const name in headers) {
diff --git a/front_end/externs.js b/front_end/externs.js
index cbe1f23..46b5282 100644
--- a/front_end/externs.js
+++ b/front_end/externs.js
@@ -1503,7 +1503,11 @@
 /** @typedef
 {{
     statusCode: number,
-    headers: (!Object.<string, string>|undefined)
+    headers: (!Object.<string, string>|undefined),
+    netError: (number|undefined),
+    netErrorName: (string|undefined),
+    urlValid: (boolean|undefined),
+    messageOverride: (string|undefined)
 }} */
 InspectorFrontendHostAPI.LoadNetworkResourceResult;
 
diff --git a/front_end/host/ResourceLoader.js b/front_end/host/ResourceLoader.js
index 9f33bbc..6e6b79b 100644
--- a/front_end/host/ResourceLoader.js
+++ b/front_end/host/ResourceLoader.js
@@ -36,30 +36,118 @@
   _boundStreams[id].write(chunk);
 };
 
+/** @typedef
+{{
+    statusCode: number,
+    netError: (number|undefined),
+    netErrorName: (string|undefined),
+    urlValid: (boolean|undefined),
+    message: (string|undefined)
+}} */
+ResourceLoader.LoadErrorDescription;
+
 /**
  * @param {string} url
  * @param {?Object.<string, string>} headers
- * @param {function(number, !Object.<string, string>, string, number)} callback
+ * @param {function(boolean, !Object.<string, string>, string, !ResourceLoader.LoadErrorDescription)} callback
  */
 export function load(url, headers, callback) {
   const stream = new Common.StringOutputStream();
   loadAsStream(url, headers, stream, mycallback);
 
   /**
-   * @param {number} statusCode
+   * @param {boolean} success
    * @param {!Object.<string, string>} headers
-   * @param {number} netError
+   * @param {!ResourceLoader.LoadErrorDescription} errorDescription
    */
-  function mycallback(statusCode, headers, netError) {
-    callback(statusCode, headers, stream.data(), netError);
+  function mycallback(success, headers, errorDescription) {
+    callback(success, headers, stream.data(), errorDescription);
   }
 }
 
 /**
+ * @param {number} netError
+ * Keep this function in sync with `net_error_list.h` on the Chromium side.
+ * @returns {string}
+ */
+function getNetErrorCategory(netError) {
+  if (netError > -100) {
+    return ls`System error`;
+  }
+  if (netError > -200) {
+    return ls`Connection error`;
+  }
+  if (netError > -300) {
+    return ls`Certificate error`;
+  }
+  if (netError > -400) {
+    return ls`HTTP error`;
+  }
+  if (netError > -500) {
+    return ls`Cache error`;
+  }
+  if (netError > -600) {
+    return ls`Signed Exchange error`;
+  }
+  if (netError > -700) {
+    return ls`FTP error`;
+  }
+  if (netError > -800) {
+    return ls`Certificate manager error`;
+  }
+  if (netError > -900) {
+    return ls`DNS resolver error`;
+  }
+  return ls`Unknown error`;
+}
+
+/**
+ * @param {number} netError
+ * @returns {boolean}
+ */
+function isHTTPError(netError) {
+  return netError <= -300 && netError > -400;
+}
+
+/**
+ * @param {!InspectorFrontendHostAPI.LoadNetworkResourceResult} response
+ * @returns {!{success:boolean, description: !ResourceLoader.LoadErrorDescription}}
+ */
+function createErrorMessageFromResponse(response) {
+  const {statusCode, netError, netErrorName, urlValid, messageOverride} = response;
+  let message = '';
+  const success = statusCode >= 200 && statusCode < 300;
+  if (typeof messageOverride === 'string') {
+    message = messageOverride;
+  } else if (!success) {
+    if (typeof netError === 'undefined') {
+      if (urlValid === false) {
+        message = ls`Invalid URL`;
+      } else {
+        message = ls`Unknown error`;
+      }
+    } else {
+      if (netError !== 0) {
+        if (isHTTPError(netError)) {
+          message += ls`HTTP error: status code ${statusCode}, ${netErrorName}`;
+        } else {
+          const errorCategory = getNetErrorCategory(netError);
+          // We don't localize here, as `errorCategory` is already localized,
+          // and `netErrorName` is an error code like 'net::ERR_CERT_AUTHORITY_INVALID'.
+          message = `${errorCategory}: ${netErrorName}`;
+        }
+      }
+    }
+  }
+  console.assert(success === (message.length === 0));
+  return {success, description: {statusCode, netError, netErrorName, urlValid, message}};
+}
+
+/**
  * @param {string} url
  * @param {?Object.<string, string>} headers
  * @param {!Common.OutputStream} stream
- * @param {function(number, !Object.<string, string>, number)=} callback
+ * @param {function(boolean, !Object.<string, string>, !ResourceLoader.LoadErrorDescription)=} callback
  */
 export const loadAsStream = function(url, headers, stream, callback) {
   const streamId = _bindOutputStream(stream);
@@ -82,7 +170,8 @@
    */
   function finishedCallback(response) {
     if (callback) {
-      callback(response.statusCode, response.headers || {}, response.netError || 0);
+      const {success, description} = createErrorMessageFromResponse(response);
+      callback(success, response.headers || {}, description);
     }
     _discardOutputStream(streamId);
   }
@@ -95,7 +184,9 @@
     finishedCallback(/** @type {!InspectorFrontendHostAPI.LoadNetworkResourceResult} */ ({statusCode: 200}));
   }
 
-  function dataURLDecodeFailed() {
-    finishedCallback(/** @type {!InspectorFrontendHostAPI.LoadNetworkResourceResult} */ ({statusCode: 404}));
+  function dataURLDecodeFailed(xhrStatus) {
+    const messageOverride = ls`Decoding Data URL failed`;
+    finishedCallback(
+        /** @type {!InspectorFrontendHostAPI.LoadNetworkResourceResult} */ ({statusCode: 404, messageOverride}));
   }
 };
diff --git a/front_end/host/host-legacy.js b/front_end/host/host-legacy.js
index dd892f7..fee065d 100644
--- a/front_end/host/host-legacy.js
+++ b/front_end/host/host-legacy.js
@@ -27,7 +27,7 @@
 /**
  * @param {string} url
  * @param {?Object.<string, string>} headers
- * @param {function(number, !Object.<string, string>, string, number)} callback
+ * @param {function(boolean, !Object.<string, string>, string, !HostModule.ResourceLoader.ResourceLoader.LoadErrorDescription)} callback
  */
 Host.ResourceLoader.load = HostModule.ResourceLoader.load;
 
@@ -35,7 +35,7 @@
  * @param {string} url
  * @param {?Object.<string, string>} headers
  * @param {!Common.OutputStream} stream
- * @param {function(number, !Object.<string, string>, number)=} callback
+ * @param {function(boolean, !Object.<string, string>, !HostModule.ResourceLoader.ResourceLoader.LoadErrorDescription)=} callback
  */
 Host.ResourceLoader.loadAsStream = HostModule.ResourceLoader.loadAsStream;
 
diff --git a/front_end/host/host_strings.grdp b/front_end/host/host_strings.grdp
index 020a2ff..e4f3d22 100644
--- a/front_end/host/host_strings.grdp
+++ b/front_end/host/host_strings.grdp
@@ -1,6 +1,45 @@
 <?xml version="1.0" encoding="utf-8"?>
 <grit-part>
+  <message name="IDS_DEVTOOLS_1f651ab3d2499f9ec45d86440334683e" desc="Name of an error category used in error messages">
+    HTTP error
+  </message>
+  <message name="IDS_DEVTOOLS_2e92ae79ff32b37fee4368a594792183" desc="Name of an error category used in error messages">
+    Connection error
+  </message>
   <message name="IDS_DEVTOOLS_92bb7e733a12d24684262f046afcc2fd" desc="Document title in Inspector Frontend Host of the DevTools window">
     <ph name="LOCKED_1">DevTools</ph> - <ph name="URL_REPLACE___HTTPS____________">$1s<ex>example.com</ex></ph>
   </message>
-</grit-part>
\ No newline at end of file
+  <message name="IDS_DEVTOOLS_92e49dde8a851253ae32a85677e834df" desc="Phrase used in error messages that carry a network error name">
+    HTTP error: status code <ph name="STATUSCODE">$1s<ex>404</ex></ph>, <ph name="NETERRORNAME">$2s<ex>net::ERR_INSUFFICIENT_RESOURCES</ex></ph>
+  </message>
+  <message name="IDS_DEVTOOLS_9c9fc26f84098ba85cdf3397fd8184c7" desc="Name of an error category used in error messages">
+    Cache error
+  </message>
+  <message name="IDS_DEVTOOLS_a1bd59ccc3b0879f006fc81bce5cbde9" desc="Name of an error category used in error messages">
+    System error
+  </message>
+  <message name="IDS_DEVTOOLS_a811fec555556024e2175b487a9e6c21" desc="Name of an error category used in error messages">
+    DNS resolver error
+  </message>
+  <message name="IDS_DEVTOOLS_a900fc2e67bbbc74a2ce79242906b842" desc="Name of an error category used in error messages">
+    FTP error
+  </message>
+  <message name="IDS_DEVTOOLS_aee9784c03b80d38d3271cde2b252b8d" desc="Name of an error category used in error messages">
+    Unknown error
+  </message>
+  <message name="IDS_DEVTOOLS_bf945532466a63b144bf9ad4dc26bde9" desc="Name of an error category used in error messages">
+    Decoding Data URL failed
+  </message>
+  <message name="IDS_DEVTOOLS_c97758a89070ad6146a7f57496d58122" desc="Name of an error category used in error messages">
+    Certificate error
+  </message>
+  <message name="IDS_DEVTOOLS_d8075977a0e561aef111639b8cd9e409" desc="Name of an error category used in error messages">
+    Certificate manager error
+  </message>
+  <message name="IDS_DEVTOOLS_e9dc27efa61d173b27795e7a83db3b7d" desc="Name of an error category used in error messages">
+    Signed Exchange error
+  </message>
+  <message name="IDS_DEVTOOLS_f9c7939a8397ee022fefee2bdb3407af" desc="Name of an error category used in error messages">
+    Invalid URL
+  </message>
+</grit-part>
diff --git a/front_end/main/MainImpl.js b/front_end/main/MainImpl.js
index 3862661..3513135 100644
--- a/front_end/main/MainImpl.js
+++ b/front_end/main/MainImpl.js
@@ -133,8 +133,6 @@
     Root.Runtime.experiments.register('nativeHeapProfiler', 'Native memory sampling heap profiler', true);
     Root.Runtime.experiments.register('protocolMonitor', 'Protocol Monitor');
     Root.Runtime.experiments.register(
-        'reportInternalNetErrorOnSourceMapLoadFail', 'Report internal net error code when a SourceMap fails to load');
-    Root.Runtime.experiments.register(
         'recordCoverageWithPerformanceTracing', 'Record coverage while performance tracing');
     Root.Runtime.experiments.register('samplingHeapProfilerTimeline', 'Sampling heap profiler timeline', true);
     Root.Runtime.experiments.register('sourceDiff', 'Source diff');
diff --git a/front_end/sdk/CompilerSourceMappingContentProvider.js b/front_end/sdk/CompilerSourceMappingContentProvider.js
index 5bcf1dd..1e98d04 100644
--- a/front_end/sdk/CompilerSourceMappingContentProvider.js
+++ b/front_end/sdk/CompilerSourceMappingContentProvider.js
@@ -72,24 +72,15 @@
    */
   requestContent() {
     return new Promise(resolve => {
-      SDK.multitargetNetworkManager.loadResource(
-          this._sourceURL,
-          /**
-         * @param {number} statusCode
-         * @param {!Object.<string, string>} _headers (unused)
-         * @param {string} content
-         * @this {CompilerSourceMappingContentProvider}
-         */
-          (statusCode, _headers, content, netError) => {
-            if (statusCode >= 400) {
-              const error = ls`Could not load content for ${this._sourceURL} (HTTP status code: ${
-                  statusCode}, net error code ${netError})`;
-              console.error(error);
-              resolve({error, isEncoded: false});
-            } else {
-              resolve({content, isEncoded: false});
-            }
-          });
+      SDK.multitargetNetworkManager.loadResource(this._sourceURL, (success, _headers, content, errorDescription) => {
+        if (!success) {
+          const error = ls`Could not load content for ${this._sourceURL} (${errorDescription.message})`;
+          console.error(error);
+          resolve({error, isEncoded: false});
+        } else {
+          resolve({content, isEncoded: false});
+        }
+      });
     });
   }
 
diff --git a/front_end/sdk/NetworkManager.js b/front_end/sdk/NetworkManager.js
index ef8a4cc..a28f921 100644
--- a/front_end/sdk/NetworkManager.js
+++ b/front_end/sdk/NetworkManager.js
@@ -28,6 +28,8 @@
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  */
 
+import {ResourceLoader} from '../host/ResourceLoader.js';  // eslint-disable-line no-unused-vars
+
 import {Cookie} from './Cookie.js';
 import {Events as NetworkRequestEvents, NetworkRequest} from './NetworkRequest.js';
 import {Capability, SDKModel, SDKModelObserver, Target} from './SDKModel.js';  // eslint-disable-line no-unused-vars
@@ -1333,7 +1335,7 @@
 
   /**
    * @param {string} url
-   * @param {function(number, !Object.<string, string>, string, number)} callback
+   * @param {function(boolean, !Object.<string, string>, string, !ResourceLoader.LoadErrorDescription)} callback
    */
   loadResource(url, callback) {
     const headers = {};
diff --git a/front_end/sdk/SourceMap.js b/front_end/sdk/SourceMap.js
index 0fc4434..f7dd03b 100644
--- a/front_end/sdk/SourceMap.js
+++ b/front_end/sdk/SourceMap.js
@@ -226,12 +226,9 @@
    */
   static async load(sourceMapURL, compiledURL) {
     let content = await new Promise((resolve, reject) => {
-      SDK.multitargetNetworkManager.loadResource(sourceMapURL, (statusCode, _headers, content, netError) => {
-        if (!content || statusCode >= 400) {
-          const showInternalError = Root.Runtime.experiments.isEnabled('reportInternalNetErrorOnSourceMapLoadFail');
-          const internalError =
-              showInternalError ? ls` (HTTP status code: ${statusCode}, net error code ${netError})` : ``;
-          const error = new Error(ls`Could not load content for ${sourceMapURL}${internalError}`);
+      SDK.multitargetNetworkManager.loadResource(sourceMapURL, (success, _headers, content, errorDescription) => {
+        if (!content || !success) {
+          const error = new Error(ls`Could not load content for ${sourceMapURL}: ${errorDescription.message}`);
           reject(error);
         } else {
           resolve(content);
diff --git a/front_end/sdk/sdk_strings.grdp b/front_end/sdk/sdk_strings.grdp
index bd00b6e..31e7d06 100644
--- a/front_end/sdk/sdk_strings.grdp
+++ b/front_end/sdk/sdk_strings.grdp
@@ -81,9 +81,6 @@
   <message name="IDS_DEVTOOLS_5e53467e9b005376370c28e92b42b6f5" desc="Title of a setting under the Rendering drawer that can be invoked through the Command Menu">
     Emulate CSS prefers-reduced-motion: reduce
   </message>
-  <message name="IDS_DEVTOOLS_66c8faee22c98da5169a40a63299f993" desc="Error message when failing to load a source map text via the network">
-    Could not load content for <ph name="SOURCEMAPURL">$1s<ex>https://example.com</ex></ph><ph name="INTERNALERROR">$2s<ex> error code 15</ex></ph>
-  </message>
   <message name="IDS_DEVTOOLS_685c645cb0cb322c5b9989eb12bada19" desc="Title of a setting under the Rendering drawer that can be invoked through the Command Menu">
     Emulate CSS media feature prefers-color-scheme
   </message>
@@ -207,6 +204,9 @@
   <message name="IDS_DEVTOOLS_7121afd196f5c52bef488d5a0f4c097b" desc="Text in the Event Listener Breakpoints Panel of the JavaScript Debugger in the Sources Panel">
     Script First Statement
   </message>
+  <message name="IDS_DEVTOOLS_716ddf802d9b9ee6899a47025e38bff7" desc="Error message when failing to fetch a resource referenced in a source map">
+    Could not load content for <ph name="THIS__SOURCEURL">$1s<ex>https://example.com/sourcemap.map</ex></ph> (<ph name="ERRORMESSAGE">$2s<ex>An error occurred</ex></ph>)
+  </message>
   <message name="IDS_DEVTOOLS_73329564760013a7824ff9d5d1af91ff" desc="Service worker version status displayed in the Threads view of the Debugging side pane in the Sources panel">
     installed
   </message>
@@ -279,9 +279,6 @@
   <message name="IDS_DEVTOOLS_a1595abbb4c3a326636dd178757cd6c1" desc="Text in DOMDebugger Model">
     Control
   </message>
-  <message name="IDS_DEVTOOLS_a4fde38e56094c9fa87b2d9079dad5b4" desc="Error message when failing to load a source map text via the network">
-    ''' (HTTP status code: <ph name="STATUSCODE"><ex>404</ex>$1s</ph>, net error code <ph name="NETERROR"><ex>-202</ex>$2s</ph>)
-  </message>
   <message name="IDS_DEVTOOLS_a720fc15eb9ec85751969e8615ace9e1" desc="Text in Server Timing">
     Duplicate parameter &quot;<ph name="PARAMNAME">$1s<ex>https</ex></ph>&quot; ignored.
   </message>
@@ -390,9 +387,6 @@
   <message name="IDS_DEVTOOLS_edb020d2175281d94054136e09a3e132" desc="Title of a setting under the Debugger category that can be invoked through the Command Menu">
     Do not pause on exceptions
   </message>
-  <message name="IDS_DEVTOOLS_ef2d5ca3248aaeb464b086e633f79d5b" desc="Error message when failing to load a script source text via the network">
-    Could not load content for <ph name="THIS__SOURCEURL"><ex>https://example.com</ex>$1s</ph> (HTTP status code: <ph name="STATUSCODE"><ex>404</ex>$2s</ph>, net error code <ph name="NETERROR"><ex>-202</ex>$3s</ph>)
-  </message>
   <message name="IDS_DEVTOOLS_efa547f7d0b9924fdc7b301838c99fad" desc="A drop-down menu option to do not emulate css media type">
     No emulation
   </message>
@@ -417,6 +411,9 @@
   <message name="IDS_DEVTOOLS_f7531e2d0ea27233ce00b5f01c5bf335" desc="A drop-down menu option to emulate css print media type">
     print
   </message>
+  <message name="IDS_DEVTOOLS_b507642e29a193b6853843992eef8c10" desc="Error message when failing to load a source map text via the network">
+    Could not load content for <ph name="SOURCEMAPURL">$1s<ex>https://example.com/sourcemap.map</ex></ph>: <ph name="ERRORMESSAGE">$2s<ex>A certificate error occured</ex></ph>
+  </message>
   <message name="IDS_DEVTOOLS_f9778c8e7464e4bb037ec2463879588f" desc="Text in DOMDebugger Model">
     DOM Mutation
   </message>