Bug 1475391 - Add mapping for CORS error types to MDN pages. r=bgrins, a=lizzard
☠☠ backed out by c22eb9a9315c ☠ ☠
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Fri, 03 Aug 2018 06:40:36 +0000
changeset 478376 4471d22def67
parent 478375 e2e1d461ee93
child 478377 c22eb9a9315c
push id9639
push userryanvm@gmail.com
push dateFri, 10 Aug 2018 20:54:33 +0000
treeherdermozilla-beta@4471d22def67 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins, lizzard
bugs1475391
milestone62.0
Bug 1475391 - Add mapping for CORS error types to MDN pages. r=bgrins, a=lizzard Differential Revision: https://phabricator.services.mozilla.com/D2557
devtools/client/webconsole/test/mochitest/browser.ini
devtools/client/webconsole/test/mochitest/browser_webconsole_cors_errors.js
devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs
devtools/server/actors/errordocs.js
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -263,16 +263,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_webconsole_context_menu_copy_link_location.js]
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
 [browser_webconsole_context_menu_copy_object.js]
 subsuite = clipboard
 [browser_webconsole_context_menu_object_in_sidebar.js]
 [browser_webconsole_context_menu_open_url.js]
 [browser_webconsole_context_menu_store_as_global.js]
+[browser_webconsole_cors_errors.js]
 [browser_webconsole_csp_ignore_reflected_xss_message.js]
 skip-if = (e10s && debug) || (e10s && os == 'win') # Bug 1221499 enabled these on windows
 [browser_webconsole_csp_violation.js]
 [browser_webconsole_cspro.js]
 [browser_webconsole_document_focus.js]
 [browser_webconsole_duplicate_errors.js]
 [browser_webconsole_error_with_longstring_stack.js]
 [browser_webconsole_error_with_unicode.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_cors_errors.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure that the different CORS error are logged to the console with the appropriate
+// "Learn more" link.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/mochitest/test-network-request.html";
+const BASE_CORS_ERROR_URL = "https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/";
+const BASE_CORS_ERROR_URL_PARAMS = new URLSearchParams({
+  utm_source: "devtools",
+  utm_medium: "firefox-cors-errors",
+  utm_campaign: "default",
+});
+
+add_task(async function() {
+  await pushPref("devtools.webconsole.filter.netxhr", true);
+
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  let onCorsMessage;
+  let message;
+
+  info(`Setting "content.cors.disable" to true to test CORSDisabled message`);
+  await pushPref("content.cors.disable", true);
+  onCorsMessage = waitForMessage(hud, "Reason: CORS disabled");
+  makeFaultyCorsCall("CORSDisabled");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSDisabled");
+  await pushPref("content.cors.disable", false);
+
+  info("Test CORSPreflightDidNotSucceed");
+  onCorsMessage = waitForMessage(hud, `CORS preflight channel did not succeed`);
+  makeFaultyCorsCall("CORSPreflightDidNotSucceed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSPreflightDidNotSucceed");
+
+  info("Test CORS did not succeed");
+  onCorsMessage = waitForMessage(hud, "Reason: CORS request did not succeed");
+  makeFaultyCorsCall("CORSDidNotSucceed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSDidNotSucceed");
+
+  info("Test CORSExternalRedirectNotAllowed");
+  onCorsMessage = waitForMessage(hud,
+    "Reason: CORS request external redirect not allowed");
+  makeFaultyCorsCall("CORSExternalRedirectNotAllowed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSExternalRedirectNotAllowed");
+
+  info("Test CORSMissingAllowOrigin");
+  onCorsMessage = waitForMessage(hud,
+    `Reason: CORS header ${quote("Access-Control-Allow-Origin")} missing`);
+  makeFaultyCorsCall("CORSMissingAllowOrigin");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMissingAllowOrigin");
+
+  info("Test CORSMultipleAllowOriginNotAllowed");
+  onCorsMessage = waitForMessage(hud,
+    `Reason: Multiple CORS header ${quote("Access-Control-Allow-Origin")} not allowed`);
+  makeFaultyCorsCall("CORSMultipleAllowOriginNotAllowed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMultipleAllowOriginNotAllowed");
+
+  info("Test CORSAllowOriginNotMatchingOrigin");
+  onCorsMessage = waitForMessage(hud, `Reason: CORS header ` +
+    `${quote("Access-Control-Allow-Origin")} does not match ${quote("mochi.test")}`);
+  makeFaultyCorsCall("CORSAllowOriginNotMatchingOrigin");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSAllowOriginNotMatchingOrigin");
+
+  info("Test CORSNotSupportingCredentials");
+  onCorsMessage = waitForMessage(hud, `Reason: Credential is not supported if the CORS ` +
+    `header ${quote("Access-Control-Allow-Origin")} is ${quote("*")}`);
+  makeFaultyCorsCall("CORSNotSupportingCredentials");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSNotSupportingCredentials");
+
+  info("Test CORSMethodNotFound");
+  onCorsMessage = waitForMessage(hud, `Reason: Did not find method in CORS header ` +
+    `${quote("Access-Control-Allow-Methods")}`);
+  makeFaultyCorsCall("CORSMethodNotFound");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMethodNotFound");
+
+  info("Test CORSMissingAllowCredentials");
+  onCorsMessage = waitForMessage(hud, `Reason: expected ${quote("true")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Credentials")}`);
+  makeFaultyCorsCall("CORSMissingAllowCredentials");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMissingAllowCredentials");
+
+  info("Test CORSInvalidAllowMethod");
+  onCorsMessage = waitForMessage(hud, `Reason: invalid token ${quote("xyz;")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Methods")}`);
+  makeFaultyCorsCall("CORSInvalidAllowMethod");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSInvalidAllowMethod");
+
+  info("Test CORSInvalidAllowHeader");
+  onCorsMessage = waitForMessage(hud, `Reason: invalid token ${quote("xyz;")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Headers")}`);
+  makeFaultyCorsCall("CORSInvalidAllowHeader");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSInvalidAllowHeader");
+
+  info("Test CORSMissingAllowHeaderFromPreflight");
+  onCorsMessage = waitForMessage(hud, `Reason: missing token ${quote("xyz")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Headers")} from CORS preflight channel`);
+  makeFaultyCorsCall("CORSMissingAllowHeaderFromPreflight");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMissingAllowHeaderFromPreflight");
+
+  // See Bug 1480671.
+  // XXX: how to make Origin to not be included in the request ?
+  // onCorsMessage = waitForMessage(hud,
+  //   `Reason: CORS header ${quote("Origin")} cannot be added`);
+  // makeFaultyCorsCall("CORSOriginHeaderNotAdded");
+  // message = await onCorsMessage;
+  // await checkCorsMessage(message, "CORSOriginHeaderNotAdded");
+
+  // See Bug 1480672.
+  // XXX: Failing with another error: Console message: Security Error: Content at
+  // http://example.com/browser/devtools/client/webconsole/test/mochitest/test-network-request.html
+  // may not load or link to file:///Users/nchevobbe/Projects/mozilla-central/devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs.
+  // info("Test CORSRequestNotHttp");
+  // onCorsMessage = waitForMessage(hud, "Reason: CORS request not http");
+  // const dir = getChromeDir(getResolvedURI(gTestPath));
+  // dir.append("sjs_cors-test-server.sjs");
+  // makeFaultyCorsCall("CORSRequestNotHttp", Services.io.newFileURI(dir).spec);
+  // message = await onCorsMessage;
+  // await checkCorsMessage(message, "CORSRequestNotHttp");
+});
+
+async function checkCorsMessage(message, category) {
+  const node = message.node;
+  ok(node.classList.contains("warn"), "The cors message has the expected classname");
+  const learnMoreLink = node.querySelector(".learn-more-link");
+  ok(learnMoreLink, "There is a Learn more link displayed");
+  const linkSimulation = await simulateLinkClick(learnMoreLink);
+  is(linkSimulation.link, getCategoryUrl(category),
+    "Click on the link opens the expected page");
+}
+
+function makeFaultyCorsCall(errorCategory, corsUrl) {
+  ContentTask.spawn(gBrowser.selectedBrowser, [errorCategory, corsUrl],
+    ([category, url]) => {
+      if (!url) {
+        const baseUrl =
+          "http://mochi.test:8888/browser/devtools/client/webconsole/test/mochitest";
+        url = `${baseUrl}/sjs_cors-test-server.sjs?corsErrorCategory=${category}`;
+      }
+
+      // Preflight request are not made for GET requests, so let's do a PUT.
+      const method = "PUT";
+      const options = { method };
+      if (category === "CORSNotSupportingCredentials"
+        || category === "CORSMissingAllowCredentials"
+      ) {
+        options.credentials = "include";
+      }
+
+      if (category === "CORSMissingAllowHeaderFromPreflight") {
+        options.headers = new content.Headers({"xyz": true});
+      }
+
+      content.fetch(url, options);
+    });
+}
+
+function quote(str) {
+  const openingQuote = String.fromCharCode(8216);
+  const closingQuote = String.fromCharCode(8217);
+  return `${openingQuote}${str}${closingQuote}`;
+}
+
+function getCategoryUrl(category) {
+  return `${BASE_CORS_ERROR_URL}${category}?${BASE_CORS_ERROR_URL_PARAMS}`;
+}
--- a/devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs
+++ b/devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs
@@ -1,17 +1,150 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 function handleRequest(request, response) {
-  response.setStatusLine(request.httpVersion, 200, "Och Aye");
+  const params = new Map(
+    request.queryString
+      .replace("?", "")
+      .split("&")
+      .map(s => s.split("="))
+  );
+
+  if (!params.has("corsErrorCategory")) {
+    response.setStatusLine(request.httpVersion, 200, "Och Aye");
+    setCacheHeaders(response);
+    response.setHeader("Access-Control-Allow-Origin", "*", false);
+    response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+    response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+    response.write("Access-Control-Allow-Origin: *");
+    return;
+  }
+
+  const category = params.get("corsErrorCategory");
+  switch (category) {
+    case "CORSDidNotSucceed":
+      corsDidNotSucceed(request, response);
+      break;
+    case "CORSExternalRedirectNotAllowed":
+      corsExternalRedirectNotAllowed(request, response);
+      break;
+    case "CORSMissingAllowOrigin":
+      corsMissingAllowOrigin(request, response);
+      break;
+    case "CORSMultipleAllowOriginNotAllowed":
+      corsMultipleOriginNotAllowed(request, response);
+      break;
+    case "CORSAllowOriginNotMatchingOrigin":
+      corsAllowOriginNotMatchingOrigin(request, response);
+      break;
+    case "CORSNotSupportingCredentials":
+      corsNotSupportingCredentials(request, response);
+      break;
+    case "CORSMethodNotFound":
+      corsMethodNotFound(request, response);
+      break;
+    case "CORSMissingAllowCredentials":
+      corsMissingAllowCredentials(request, response);
+      break;
+    case "CORSPreflightDidNotSucceed":
+      corsPreflightDidNotSucceed(request, response);
+      break;
+    case "CORSInvalidAllowMethod":
+      corsInvalidAllowMethod(request, response);
+      break;
+    case "CORSInvalidAllowHeader":
+      corsInvalidAllowHeader(request, response);
+      break;
+    case "CORSMissingAllowHeaderFromPreflight":
+      corsMissingAllowHeaderFromPreflight(request, response);
+      break;
+  }
+}
+
+function corsDidNotSucceed(request, response) {
+  setCacheHeaders(response);
+  response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+  response.setHeader("Location", "http://example.com");
+}
 
+function corsExternalRedirectNotAllowed(request, response) {
+  response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+  response.setHeader("Access-Control-Allow-Origin", "*", false);
+  response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+  response.setHeader("Location", "http://redirect.test/");
+}
+
+function corsMissingAllowOrigin(request, response) {
+  setCacheHeaders(response);
+  response.setStatusLine(request.httpVersion, 200, "corsMissingAllowOrigin");
+}
+
+function corsMultipleOriginNotAllowed(request, response) {
+  // We can't set the same header twice with response.setHeader, so we need to seizePower
+  // and write the response manually.
+  response.seizePower();
+  response.write("HTTP/1.0 200 OK\r\n");
+  response.write("Content-Type: text/plain\r\n");
+  response.write("Access-Control-Allow-Origin: *\r\n");
+  response.write("Access-Control-Allow-Origin: mochi.test\r\n");
+  response.write("\r\n");
+  response.finish();
+  setCacheHeaders(response);
+}
+
+function corsAllowOriginNotMatchingOrigin(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsAllowOriginNotMatchingOrigin");
+  response.setHeader("Access-Control-Allow-Origin", "mochi.test");
+}
+
+function corsNotSupportingCredentials(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsNotSupportingCredentials");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+}
+
+function corsMethodNotFound(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsMethodNotFound");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  // Will make the request fail since it is a "PUT".
+  response.setHeader("Access-Control-Allow-Methods", "POST");
+}
+
+function corsMissingAllowCredentials(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsMissingAllowCredentials");
+  // Need to set an explicit origin (i.e. not "*") to make the request fail.
+  response.setHeader("Access-Control-Allow-Origin", "http://example.com");
+}
+
+function corsPreflightDidNotSucceed(request, response) {
+  const isPreflight = request.method == "OPTIONS";
+  if (isPreflight) {
+    response.setStatusLine(request.httpVersion, 500, "Preflight fail");
+    response.setHeader("Access-Control-Allow-Origin", "*");
+  }
+}
+
+function corsInvalidAllowMethod(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowMethod");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  response.setHeader("Access-Control-Allow-Methods", "xyz;");
+}
+
+function corsInvalidAllowHeader(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowHeader");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  response.setHeader("Access-Control-Allow-Methods", "PUT");
+  response.setHeader("Access-Control-Allow-Headers", "xyz;");
+}
+
+function corsMissingAllowHeaderFromPreflight(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsMissingAllowHeaderFromPreflight");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  response.setHeader("Access-Control-Allow-Methods", "PUT");
+}
+
+function setCacheHeaders(response) {
   response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
   response.setHeader("Pragma", "no-cache");
   response.setHeader("Expires", "0");
-
-  response.setHeader("Access-Control-Allow-Origin", "*", false);
-  response.setHeader("Access-Control-Allow-Headers", "content-type", false);
-
-  response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
-
-  response.write("Access-Control-Allow-Origin: *");
 }
--- a/devtools/server/actors/errordocs.js
+++ b/devtools/server/actors/errordocs.js
@@ -4,19 +4,20 @@
 
 /**
  * A mapping of error message names to external documentation. Any error message
  * included here will be displayed alongside its link in the web console.
  */
 
 "use strict";
 
-const baseURL = "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/";
+const baseErrorURL = "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/";
 const params =
   "?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default";
+
 const ErrorDocs = {
   JSMSG_READ_ONLY: "Read-only",
   JSMSG_BAD_ARRAY_LENGTH: "Invalid_array_length",
   JSMSG_NEGATIVE_REPETITION_COUNT: "Negative_repetition_count",
   JSMSG_RESULTING_STRING_TOO_LARGE: "Resulting_string_too_large",
   JSMSG_BAD_RADIX: "Bad_radix",
   JSMSG_PRECISION_RANGE: "Precision_range",
   JSMSG_STMT_AFTER_RETURN: "Stmt_after_return",
@@ -103,24 +104,50 @@ const ErrorCategories = {
   "Invalid HPKP Headers": PUBLIC_KEY_PINS_LEARN_MORE,
   "Invalid HSTS Headers": STRICT_TRANSPORT_SECURITY_LEARN_MORE,
   "SHA-1 Signature": WEAK_SIGNATURE_ALGORITHM_LEARN_MORE,
   "Tracking Protection": TRACKING_PROTECTION_LEARN_MORE,
   "MIMEMISMATCH": MIME_TYPE_MISMATCH_LEARN_MORE,
   "source map": SOURCE_MAP_LEARN_MORE,
 };
 
+const baseCorsErrorUrl = "https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/";
+const corsParams =
+  "?utm_source=devtools&utm_medium=firefox-cors-errors&utm_campaign=default";
+const CorsErrorDocs = {
+  CORSDisabled: "CORSDisabled",
+  CORSDidNotSucceed: "CORSDidNotSucceed",
+  CORSOriginHeaderNotAdded: "CORSOriginHeaderNotAdded",
+  CORSExternalRedirectNotAllowed: "CORSExternalRedirectNotAllowed",
+  CORSRequestNotHttp: "CORSRequestNotHttp",
+  CORSMissingAllowOrigin: "CORSMissingAllowOrigin",
+  CORSMultipleAllowOriginNotAllowed: "CORSMultipleAllowOriginNotAllowed",
+  CORSAllowOriginNotMatchingOrigin: "CORSAllowOriginNotMatchingOrigin",
+  CORSNotSupportingCredentials: "CORSNotSupportingCredentials",
+  CORSMethodNotFound: "CORSMethodNotFound",
+  CORSMissingAllowCredentials: "CORSMissingAllowCredentials",
+  CORSPreflightDidNotSucceed: "CORSPreflightDidNotSucceed",
+  CORSInvalidAllowMethod: "CORSInvalidAllowMethod",
+  CORSInvalidAllowHeader: "CORSInvalidAllowHeader",
+  CORSMissingAllowHeaderFromPreflight: "CORSMissingAllowHeaderFromPreflight",
+};
+
 exports.GetURL = (error) => {
   if (!error) {
     return undefined;
   }
 
   const doc = ErrorDocs[error.errorMessageName];
   if (doc) {
-    return baseURL + doc + params;
+    return baseErrorURL + doc + params;
+  }
+
+  const corsDoc = CorsErrorDocs[error.category];
+  if (corsDoc) {
+    return baseCorsErrorUrl + corsDoc + corsParams;
   }
 
   const categoryURL = ErrorCategories[error.category];
   if (categoryURL) {
     return categoryURL + params;
   }
   return undefined;
 };