Bug 1545421 - New nsresult error codes for 407, 502 and 504 http response codes returned by proxies + test, r=dragana a=jcristau
authorHonza Bambas <honzab.moz@firemni.cz>
Fri, 31 May 2019 17:29:53 +0000
changeset 537077 66f621aa4cfa7d3edb7a410a8c98c9b3d8c8a13c
parent 537076 5c9288e6d56a6f27610ceca5e418431fca93119f
child 537078 90725f025a3b433f0bf0d69359905b51c3711d6a
push id2082
push userffxbld-merge
push dateMon, 01 Jul 2019 08:34:18 +0000
treeherdermozilla-release@2fb19d0466d2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdragana, jcristau
bugs1545421
milestone68.0
Bug 1545421 - New nsresult error codes for 407, 502 and 504 http response codes returned by proxies + test, r=dragana a=jcristau Differential Revision: https://phabricator.services.mozilla.com/D32817
docshell/base/nsDocShell.cpp
dom/push/test/xpcshell/head-http2.js
js/xpconnect/src/xpc.msg
netwerk/protocol/http/Http2Stream.cpp
netwerk/protocol/http/Http2Stream.h
netwerk/protocol/http/TunnelUtils.cpp
netwerk/protocol/http/TunnelUtils.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/test/unit/test_http1-proxy.js
netwerk/test/unit/test_http2-proxy.js
netwerk/test/unit/test_proxy-authorization-via-proxyinfo.js
netwerk/test/unit/xpcshell.ini
testing/xpcshell/moz-http2/moz-http2.js
testing/xpcshell/runxpcshelltests.py
xpcom/base/ErrorList.py
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -4082,25 +4082,27 @@ nsDocShell::DisplayLoadError(nsresult aE
     // Get the host
     nsAutoCString host;
     nsCOMPtr<nsIURI> innermostURI = NS_GetInnermostURI(aURI);
     innermostURI->GetHost(host);
     CopyUTF8toUTF16(host, formatStrs[0]);
     formatStrCount = 1;
     errorDescriptionID = "dnsNotFound2";
     error = "dnsNotFound";
-  } else if (NS_ERROR_CONNECTION_REFUSED == aError) {
+  } else if (NS_ERROR_CONNECTION_REFUSED == aError ||
+             NS_ERROR_PROXY_BAD_GATEWAY == aError) {
     NS_ENSURE_ARG_POINTER(aURI);
     addHostPort = true;
     error = "connectionFailure";
   } else if (NS_ERROR_NET_INTERRUPT == aError) {
     NS_ENSURE_ARG_POINTER(aURI);
     addHostPort = true;
     error = "netInterrupt";
-  } else if (NS_ERROR_NET_TIMEOUT == aError) {
+  } else if (NS_ERROR_NET_TIMEOUT == aError ||
+             NS_ERROR_PROXY_GATEWAY_TIMEOUT == aError) {
     NS_ENSURE_ARG_POINTER(aURI);
     // Get the host
     nsAutoCString host;
     aURI->GetHost(host);
     CopyUTF8toUTF16(host, formatStrs[0]);
     formatStrCount = 1;
     error = "netTimeout";
   } else if (NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION == aError ||
@@ -4319,16 +4321,17 @@ nsDocShell::DisplayLoadError(nsresult aE
         addHostPort = true;
         error = "deniedPortAccess";
         break;
       case NS_ERROR_UNKNOWN_PROXY_HOST:
         // Proxy hostname could not be resolved.
         error = "proxyResolveFailure";
         break;
       case NS_ERROR_PROXY_CONNECTION_REFUSED:
+      case NS_ERROR_PROXY_AUTHENTICATION_FAILED:
         // Proxy connection was refused.
         error = "proxyConnectFailure";
         break;
       case NS_ERROR_INVALID_CONTENT_ENCODING:
         // Bad Content Encoding.
         error = "contentEncodingError";
         break;
       case NS_ERROR_REMOTE_XUL:
@@ -6928,25 +6931,28 @@ nsresult nsDocShell::EndPageLoad(nsIWebP
     // Well, fixup didn't work :-(
     // It is time to throw an error dialog box, and be done with it...
 
     // Errors to be shown only on top-level frames
     if ((aStatus == NS_ERROR_UNKNOWN_HOST ||
          aStatus == NS_ERROR_CONNECTION_REFUSED ||
          aStatus == NS_ERROR_UNKNOWN_PROXY_HOST ||
          aStatus == NS_ERROR_PROXY_CONNECTION_REFUSED ||
+         aStatus == NS_ERROR_PROXY_AUTHENTICATION_FAILED ||
          aStatus == NS_ERROR_BLOCKED_BY_POLICY) &&
         (isTopFrame || UseErrorPages())) {
       DisplayLoadError(aStatus, url, nullptr, aChannel);
     } else if (aStatus == NS_ERROR_NET_TIMEOUT ||
+               aStatus == NS_ERROR_PROXY_GATEWAY_TIMEOUT ||
                aStatus == NS_ERROR_REDIRECT_LOOP ||
                aStatus == NS_ERROR_UNKNOWN_SOCKET_TYPE ||
                aStatus == NS_ERROR_NET_INTERRUPT ||
-               aStatus == NS_ERROR_NET_RESET || aStatus == NS_ERROR_OFFLINE ||
-               aStatus == NS_ERROR_MALWARE_URI ||
+               aStatus == NS_ERROR_NET_RESET ||
+               aStatus == NS_ERROR_PROXY_BAD_GATEWAY ||
+               aStatus == NS_ERROR_OFFLINE || aStatus == NS_ERROR_MALWARE_URI ||
                aStatus == NS_ERROR_PHISHING_URI ||
                aStatus == NS_ERROR_UNWANTED_URI ||
                aStatus == NS_ERROR_HARMFUL_URI ||
                aStatus == NS_ERROR_UNSAFE_CONTENT_TYPE ||
                aStatus == NS_ERROR_REMOTE_XUL ||
                aStatus == NS_ERROR_INTERCEPTION_FAILED ||
                aStatus == NS_ERROR_NET_INADEQUATE_SECURITY ||
                aStatus == NS_ERROR_NET_HTTP2_SENT_GOAWAY ||
--- a/dom/push/test/xpcshell/head-http2.js
+++ b/dom/push/test/xpcshell/head-http2.js
@@ -7,16 +7,27 @@ function getTestServerPort() {
   let port = parseInt(portEnv, 10);
   if (!Number.isFinite(port) || port < 1 || port > 65535) {
     throw new Error(`Invalid port in MOZHTTP2_PORT env var: ${portEnv}`);
   }
   info(`Using HTTP/2 server on port ${port}`);
   return port;
 }
 
+function getTestProxyPort() {
+  let portEnv = Cc["@mozilla.org/process/environment;1"]
+    .getService(Ci.nsIEnvironment).get("MOZHTTP2_PROXY_PORT");
+  let port = parseInt(portEnv, 10);
+  if (!Number.isFinite(port) || port < 1 || port > 65535) {
+    throw new Error(`Invalid port in MOZHTTP2_PROXY_PORT env var: ${portEnv}`);
+  }
+  info(`Using HTTP/2 proxy on port ${port}`);
+  return port;
+}
+
 function readFile(file) {
   let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
                   .createInstance(Ci.nsIFileInputStream);
   fstream.init(file, -1, 0, 0);
   let data = NetUtil.readInputStreamToString(fstream, fstream.available());
   fstream.close();
   return data;
 }
--- a/js/xpconnect/src/xpc.msg
+++ b/js/xpconnect/src/xpc.msg
@@ -140,16 +140,19 @@ XPC_MSG_DEF(NS_ERROR_IN_PROGRESS        
 XPC_MSG_DEF(NS_ERROR_ALREADY_OPENED                 , "Channel is already open")
 XPC_MSG_DEF(NS_ERROR_INVALID_CONTENT_ENCODING       , "The content encoding of the source document is incorrect")
 XPC_MSG_DEF(NS_ERROR_CORRUPTED_CONTENT              , "Corrupted content received from server (potentially MIME type mismatch because of 'X-Content-Type-Options: nosniff')")
 XPC_MSG_DEF(NS_ERROR_FIRST_HEADER_FIELD_COMPONENT_EMPTY, "Couldn't extract first component from potentially corrupted header field")
 XPC_MSG_DEF(NS_ERROR_ALREADY_CONNECTED              , "The connection is already established")
 XPC_MSG_DEF(NS_ERROR_NOT_CONNECTED                  , "The connection does not exist")
 XPC_MSG_DEF(NS_ERROR_CONNECTION_REFUSED             , "The connection was refused")
 XPC_MSG_DEF(NS_ERROR_PROXY_CONNECTION_REFUSED       , "The connection to the proxy server was refused")
+XPC_MSG_DEF(NS_ERROR_PROXY_AUTHENTICATION_FAILED    , "The proxy requires authentication")
+XPC_MSG_DEF(NS_ERROR_PROXY_BAD_GATEWAY              , "The request failed on the proxy")
+XPC_MSG_DEF(NS_ERROR_PROXY_GATEWAY_TIMEOUT          , "The request timed out on the proxy")
 XPC_MSG_DEF(NS_ERROR_NET_TIMEOUT                    , "The connection has timed out")
 XPC_MSG_DEF(NS_ERROR_OFFLINE                        , "The requested action could not be completed in the offline state")
 XPC_MSG_DEF(NS_ERROR_PORT_ACCESS_NOT_ALLOWED        , "Establishing a connection to an unsafe or otherwise banned port was prohibited")
 XPC_MSG_DEF(NS_ERROR_NET_RESET                      , "The connection was established, but no data was ever received")
 XPC_MSG_DEF(NS_ERROR_NET_INTERRUPT                  , "The connection was established, but the data transfer was interrupted")
 XPC_MSG_DEF(NS_ERROR_NET_PARTIAL_TRANSFER           , "A transfer was only partially done when it completed")
 XPC_MSG_DEF(NS_ERROR_NOT_RESUMABLE                  , "This request is not resumable, but it was tried to resume it, or to request resume-specific data")
 XPC_MSG_DEF(NS_ERROR_ENTITY_CHANGED                 , "It was attempted to resume the request, but the entity has changed in the meantime")
--- a/netwerk/protocol/http/Http2Stream.cpp
+++ b/netwerk/protocol/http/Http2Stream.cpp
@@ -1033,17 +1033,17 @@ nsresult Http2Stream::ConvertResponseHea
 
   LOG3(("Http2Stream::ConvertResponseHeaders %p response code %d\n", this,
         httpResponseCode));
   if (mIsTunnel) {
     LOG3(("Http2Stream %p Tunnel Response code %d", this, httpResponseCode));
     if ((httpResponseCode / 100) != 2) {
       MapStreamToPlainText();
     }
-    MapStreamToHttpConnection();
+    MapStreamToHttpConnection(httpResponseCode);
     ClearTransactionsBlockedOnTunnel();
   } else if (mIsWebsocket) {
     LOG3(("Http2Stream %p websocket response code %d", this, httpResponseCode));
     if (httpResponseCode == 200) {
       MapStreamToHttpConnection();
     }
   }
 
@@ -1596,22 +1596,24 @@ void Http2Stream::ClearTransactionsBlock
 void Http2Stream::MapStreamToPlainText() {
   RefPtr<SpdyConnectTransaction> qiTrans(
       mTransaction->QuerySpdyConnectTransaction());
   MOZ_ASSERT(qiTrans);
   mPlainTextTunnel = true;
   qiTrans->ForcePlainText();
 }
 
-void Http2Stream::MapStreamToHttpConnection() {
+void Http2Stream::MapStreamToHttpConnection(int32_t httpResponseCode) {
   RefPtr<SpdyConnectTransaction> qiTrans(
       mTransaction->QuerySpdyConnectTransaction());
   MOZ_ASSERT(qiTrans);
+
   qiTrans->MapStreamToHttpConnection(mSocketTransport,
-                                     mTransaction->ConnectionInfo());
+                                     mTransaction->ConnectionInfo(),
+                                     mIsTunnel ? httpResponseCode : -1);
 }
 
 // -----------------------------------------------------------------------------
 // mirror nsAHttpTransaction
 // -----------------------------------------------------------------------------
 
 bool Http2Stream::Do0RTT() {
   MOZ_ASSERT(mTransaction);
--- a/netwerk/protocol/http/Http2Stream.h
+++ b/netwerk/protocol/http/Http2Stream.h
@@ -364,17 +364,17 @@ class Http2Stream : public nsAHttpSegmen
 
   /// connect tunnels
  public:
   bool IsTunnel() { return mIsTunnel; }
 
  private:
   void ClearTransactionsBlockedOnTunnel();
   void MapStreamToPlainText();
-  void MapStreamToHttpConnection();
+  void MapStreamToHttpConnection(int32_t httpResponseCode = -1);
 
   bool mIsTunnel;
   bool mPlainTextTunnel;
 
   /// websockets
  public:
   bool IsWebsocket() { return mIsWebsocket; }
 
--- a/netwerk/protocol/http/TunnelUtils.cpp
+++ b/netwerk/protocol/http/TunnelUtils.cpp
@@ -1033,28 +1033,46 @@ void SpdyConnectTransaction::ForcePlainT
   MOZ_ASSERT(!mForcePlainText);
   MOZ_ASSERT(!mTunnelTransport, "call before mapstreamtohttpconnection");
   MOZ_ASSERT(!mIsWebsocket);
 
   mForcePlainText = true;
 }
 
 void SpdyConnectTransaction::MapStreamToHttpConnection(
-    nsISocketTransport* aTransport, nsHttpConnectionInfo* aConnInfo) {
+    nsISocketTransport* aTransport, nsHttpConnectionInfo* aConnInfo,
+    int32_t httpResponseCode) {
   mConnInfo = aConnInfo;
 
   mTunnelTransport = new SocketTransportShim(aTransport, mIsWebsocket);
   mTunnelStreamIn = new InputStreamShim(this, mIsWebsocket);
   mTunnelStreamOut = new OutputStreamShim(this, mIsWebsocket);
   mTunneledConn = new nsHttpConnection();
 
+  switch (httpResponseCode) {
+    case 404:
+      CreateShimError(NS_ERROR_UNKNOWN_HOST);
+      break;
+    case 407:
+      CreateShimError(NS_ERROR_PROXY_AUTHENTICATION_FAILED);
+      break;
+    case 502:
+      CreateShimError(NS_ERROR_PROXY_BAD_GATEWAY);
+      break;
+    case 504:
+      CreateShimError(NS_ERROR_PROXY_GATEWAY_TIMEOUT);
+      break;
+    default:
+      break;
+  }
+
   // this new http connection has a specific hashkey (i.e. to a particular
   // host via the tunnel) and is associated with the tunnel streams
-  LOG(("SpdyConnectTransaction new httpconnection %p %s\n", mTunneledConn.get(),
-       aConnInfo->HashKey().get()));
+  LOG(("SpdyConnectTransaction %p new httpconnection %p %s\n", this,
+       mTunneledConn.get(), aConnInfo->HashKey().get()));
 
   nsCOMPtr<nsIInterfaceRequestor> callbacks;
   GetSecurityCallbacks(getter_AddRefs(callbacks));
   mTunneledConn->SetTransactionCaps(Caps());
   MOZ_ASSERT(aConnInfo->UsingHttpsProxy() || mIsWebsocket);
   TimeDuration rtt = TimeStamp::Now() - mTimestampSyn;
   DebugOnly<nsresult> rv = mTunneledConn->Init(
       aConnInfo, gHttpHandler->ConnMgr()->MaxRequestDelay(), mTunnelTransport,
@@ -1197,16 +1215,19 @@ nsresult SpdyConnectTransaction::ReadSeg
     rv = Flush(count, &subtotal);
     *countRead += subtotal;
   }
 
   return rv;
 }
 
 void SpdyConnectTransaction::CreateShimError(nsresult code) {
+  LOG(("SpdyConnectTransaction::CreateShimError %p 0x%08" PRIx32, this,
+       static_cast<uint32_t>(code)));
+
   MOZ_ASSERT(OnSocketThread(), "not on socket thread");
   MOZ_ASSERT(NS_FAILED(code));
 
   MOZ_ASSERT(!mCreateShimErrorCalled);
   if (mCreateShimErrorCalled) {
     return;
   }
   mCreateShimErrorCalled = true;
--- a/netwerk/protocol/http/TunnelUtils.h
+++ b/netwerk/protocol/http/TunnelUtils.h
@@ -202,17 +202,18 @@ class SpdyConnectTransaction final : pub
     return this;
   }
 
   // A transaction is forced into plaintext when it is intended to be used as a
   // CONNECT tunnel but the setup fails. The plaintext only carries the CONNECT
   // error.
   void ForcePlainText();
   void MapStreamToHttpConnection(nsISocketTransport* aTransport,
-                                 nsHttpConnectionInfo* aConnInfo);
+                                 nsHttpConnectionInfo* aConnInfo,
+                                 int32_t httpResponseCode);
 
   MOZ_MUST_USE nsresult ReadSegments(nsAHttpSegmentReader* reader,
                                      uint32_t count, uint32_t* countRead) final;
   MOZ_MUST_USE nsresult WriteSegments(nsAHttpSegmentWriter* writer,
                                       uint32_t count,
                                       uint32_t* countWritten) final;
   nsHttpRequestHead* RequestHead() final;
   void Close(nsresult reason) final;
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -1947,49 +1947,53 @@ nsresult nsHttpChannel::ProcessFailedPro
     case 308:
       // Bad redirect: not top-level, or it's a POST, bad/missing Location,
       // or ProcessRedirect() failed for some other reason.  Legal
       // redirects that fail because site not available, etc., are handled
       // elsewhere, in the regular codepath.
       rv = NS_ERROR_CONNECTION_REFUSED;
       break;
     case 403:  // HTTP/1.1: "Forbidden"
-    case 407:  // ProcessAuthentication() failed
     case 501:  // HTTP/1.1: "Not Implemented"
       // user sees boilerplate Mozilla "Proxy Refused Connection" page.
       rv = NS_ERROR_PROXY_CONNECTION_REFUSED;
       break;
-    // Squid sends 404 if DNS fails (regular 404 from target is tunneled)
+    case 407:  // ProcessAuthentication() failed (e.g. no header)
+      rv = NS_ERROR_PROXY_AUTHENTICATION_FAILED;
+      break;
+      // Squid sends 404 if DNS fails (regular 404 from target is tunneled)
     case 404:  // HTTP/1.1: "Not Found"
-    // RFC 2616: "some deployed proxies are known to return 400 or 500 when
-    // DNS lookups time out."  (Squid uses 500 if it runs out of sockets: so
-    // we have a conflict here).
+               // RFC 2616: "some deployed proxies are known to return 400 or
+               // 500 when DNS lookups time out."  (Squid uses 500 if it runs
+               // out of sockets: so we have a conflict here).
     case 400:  // HTTP/1.1 "Bad Request"
     case 500:  // HTTP/1.1: "Internal Server Error"
       /* User sees: "Address Not Found: Firefox can't find the server at
        * www.foo.com."
        */
       rv = NS_ERROR_UNKNOWN_HOST;
       break;
     case 502:  // HTTP/1.1: "Bad Gateway" (invalid resp from target server)
-    // Squid returns 503 if target request fails for anything but DNS.
+      rv = NS_ERROR_PROXY_BAD_GATEWAY;
+      break;
     case 503:  // HTTP/1.1: "Service Unavailable"
+      // Squid returns 503 if target request fails for anything but DNS.
       /* User sees: "Failed to Connect:
        *  Firefox can't establish a connection to the server at
        *  www.foo.com.  Though the site seems valid, the browser
        *  was unable to establish a connection."
        */
       rv = NS_ERROR_CONNECTION_REFUSED;
       break;
     // RFC 2616 uses 504 for both DNS and target timeout, so not clear what to
     // do here: picking target timeout, as DNS covered by 400/404/500
     case 504:  // HTTP/1.1: "Gateway Timeout"
       // user sees: "Network Timeout: The server at www.foo.com
       //              is taking too long to respond."
-      rv = NS_ERROR_NET_TIMEOUT;
+      rv = NS_ERROR_PROXY_GATEWAY_TIMEOUT;
       break;
     // Confused proxy server or malicious response
     default:
       rv = NS_ERROR_PROXY_CONNECTION_REFUSED;
       break;
   }
   LOG(("Cancelling failed proxy CONNECT [this=%p httpStatus=%u]\n", this,
        httpStatus));
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_http1-proxy.js
@@ -0,0 +1,176 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test checks following expectations when using HTTP/1 proxy:
+ *
+ * - check we are seeing expected nsresult error codes on channels
+ *   (nsIChannel.status) corresponding to different proxy status code
+ *   responses (502, 504, 407, ...)
+ * - check we don't try to ask for credentials or otherwise authenticate to
+ *   the proxy when 407 is returned and there is no Proxy-Authenticate
+ *   response header sent
+ */
+
+const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+
+const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+let server_port;
+let http_server;
+
+class ProxyFilter {
+  constructor(type, host, port, flags) {
+    this._type = type;
+    this._host = host;
+    this._port = port;
+    this._flags = flags;
+    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyFilter]);
+  }
+  applyFilter(pps, uri, pi, cb) {
+    if (uri.spec.match(/(\/proxy-session-counter)/)) {
+      cb.onProxyFilterResult(pi);
+      return;
+    }
+    cb.onProxyFilterResult(pps.newProxyInfo(
+      this._type, this._host, this._port,
+      "", "", this._flags, 1000, null));
+  }
+};
+
+class UnxpectedAuthPrompt2 {
+  constructor(signal) {
+    this.signal = signal;
+    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIAuthPrompt2]);
+  }
+  asyncPromptAuth() {
+    this.signal.triggered = true;
+    throw Cr.ERROR_UNEXPECTED;
+  }
+};
+
+class AuthRequestor {
+  constructor(prompt) {
+    this.prompt = prompt;
+    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIInterfaceRequestor]);
+  }
+  getInterface(iid) {
+    if (iid.equals(Ci.nsIAuthPrompt2)) {
+      return this.prompt();
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+};
+
+function make_channel(url) {
+  return NetUtil.newChannel({
+    uri: url,
+    loadUsingSystemPrincipal: true,
+    // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types
+    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+  });
+}
+
+function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) {
+  return new Promise(resolve => {
+    channel.asyncOpen(new ChannelListener((request, data) => {
+      request.QueryInterface(Ci.nsIHttpChannel);
+      const status = request.status;
+      const http_code = status ? undefined : request.responseStatus;
+
+      resolve({ status, http_code, data });
+    }, null, flags));
+  });
+}
+
+function connect_handler(request, response) {
+  Assert.equal(request.method, "CONNECT");
+
+  switch (request.host) {
+    case "404.example.com":
+      response.setStatusLine(request.httpVersion, 404, "Not found");
+      break;
+    case "407.example.com":
+      response.setStatusLine(request.httpVersion, 407, "Authenticate");
+      // And deliberately no Proxy-Authenticate header
+      break;
+    case "502.example.com":
+      response.setStatusLine(request.httpVersion, 502, "Bad Gateway");
+      break;
+    case "504.example.com":
+      response.setStatusLine(request.httpVersion, 504, "Gateway timeout");
+      break;
+    default:
+      response.setStatusLine(request.httpVersion, 500, "I am dumb");
+  }
+}
+
+add_task(async function setup() {
+  http_server = new HttpServer();
+  http_server.identity.add("https", "404.example.com", 443);
+  http_server.identity.add("https", "407.example.com", 443);
+  http_server.identity.add("https", "502.example.com", 443);
+  http_server.identity.add("https", "504.example.com", 443);
+  http_server.registerPathHandler("CONNECT", connect_handler);
+  http_server.start(-1);
+  server_port = http_server.identity.primaryPort;
+
+  // make all native resolve calls "secretly" resolve localhost instead
+  Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+  pps.registerFilter(new ProxyFilter("http", "localhost", server_port, 0), 10);
+});
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("network.dns.native-is-localhost");
+});
+
+/**
+ * Test series beginning.
+ */
+
+// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error
+// code from the channel and not try to ask for any credentials.
+add_task(async function proxy_auth_failure() {
+  const chan = make_channel(`https://407.example.com/`);
+  const auth_prompt = { triggered: false };
+  chan.notificationCallbacks = new AuthRequestor(() => new UnxpectedAuthPrompt2(auth_prompt));
+  const { status, http_code } = await get_response(chan, CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED);
+  Assert.equal(http_code, undefined);
+  Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger");
+});
+
+// 502 Bad gateway code returned by the proxy.
+add_task(async function proxy_bad_gateway_failure() {
+  const { status, http_code } = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+  Assert.equal(http_code, undefined);
+});
+
+// 504 Gateway timeout code returned by the proxy.
+add_task(async function proxy_gateway_timeout_failure() {
+  const { status, http_code } = await get_response(make_channel(`https://504.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT);
+  Assert.equal(http_code, undefined);
+});
+
+// 404 Not Found means the proxy could not resolve the host.
+add_task(async function proxy_host_not_found_failure() {
+  const { status, http_code } = await get_response(make_channel(`https://404.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST);
+  Assert.equal(http_code, undefined);
+});
+
+add_task(async function shutdown() {
+  await new Promise(resolve => {
+    http_server.stop(resolve);
+  });
+});
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_http2-proxy.js
@@ -0,0 +1,266 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test checks following expectations when using HTTP/2 proxy:
+ *
+ * - when we request https access, we don't create different sessions for
+ *   different origins, only new tunnels inside a single session
+ * - when the isolation key (`proxy_isolation`) is changed, new single session
+ *   is created for new requests to same origins as before
+ * - error code returned from the tunnel (a proxy error - not end-server
+ *   error!) doesn't kill the existing session
+ * - check we are seeing expected nsresult error codes on channels
+ *   (nsIChannel.status) corresponding to different proxy status code
+ *   responses (502, 504, 407, ...)
+ * - check we don't try to ask for credentials or otherwise authenticate to
+ *   the proxy when 407 is returned and there is no Proxy-Authenticate
+ *   response header sent
+ */
+
+const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
+
+let proxy_port;
+let server_port;
+
+// See moz-http2
+const proxy_auth = 'authorization-token';
+let proxy_isolation;
+
+class ProxyFilter {
+  constructor(type, host, port, flags) {
+    this._type = type;
+    this._host = host;
+    this._port = port;
+    this._flags = flags;
+    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyFilter]);
+  }
+  applyFilter(pps, uri, pi, cb) {
+    if (uri.spec.match(/(\/proxy-session-counter)/)) {
+      cb.onProxyFilterResult(pi);
+      return;
+    }
+    cb.onProxyFilterResult(pps.newProxyInfo(
+      this._type, this._host, this._port,
+      proxy_auth, proxy_isolation, this._flags, 1000, null));
+  }
+};
+
+class UnxpectedAuthPrompt2 {
+  constructor(signal) {
+    this.signal = signal;
+    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIAuthPrompt2]);
+  }
+  asyncPromptAuth() {
+    this.signal.triggered = true;
+    throw Cr.ERROR_UNEXPECTED;
+  }
+};
+
+class AuthRequestor {
+  constructor(prompt) {
+    this.prompt = prompt;
+    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIInterfaceRequestor]);
+  }
+  getInterface(iid) {
+    if (iid.equals(Ci.nsIAuthPrompt2)) {
+      return this.prompt();
+    }
+    throw Cr.NS_ERROR_NO_INTERFACE;
+  }
+};
+
+function make_channel(url) {
+  return NetUtil.newChannel({
+    uri: url,
+    loadUsingSystemPrincipal: true,
+    // Using TYPE_DOCUMENT for the authentication dialog test, it'd be blocked for other types
+    contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+  });
+}
+
+function get_response(channel, flags = CL_ALLOW_UNKNOWN_CL) {
+  return new Promise(resolve => {
+    channel.asyncOpen(new ChannelListener((request, data) => {
+      request.QueryInterface(Ci.nsIHttpChannel);
+      const status = request.status;
+      const http_code = status ? undefined : request.responseStatus;
+
+      resolve({ status, http_code, data });
+    }, null, flags));
+  });
+}
+
+let initial_session_count = 0;
+
+function proxy_session_counter() {
+  return new Promise(async resolve => {
+    const channel = make_channel(`https://localhost:${server_port}/proxy-session-counter`);
+    const { data } = await get_response(channel);
+    resolve(parseInt(data) - initial_session_count);
+  });
+}
+
+add_task(async function setup() {
+  const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+  server_port = env.get("MOZHTTP2_PORT");
+  Assert.notEqual(server_port, null);
+  proxy_port = env.get("MOZHTTP2_PROXY_PORT");
+  Assert.notEqual(proxy_port, null);
+
+  // Set to allow the cert presented by our H2 server
+  do_get_profile();
+
+  Services.prefs.setBoolPref("network.http.spdy.enabled", true);
+  Services.prefs.setBoolPref("network.http.spdy.enabled.http2", true);
+
+  // make all native resolve calls "secretly" resolve localhost instead
+  Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+  // The moz-http2 cert is for foo.example.com and is signed by http2-ca.pem
+  // so add that cert to the trust list as a signing cert.
+  let certdb = Cc["@mozilla.org/security/x509certdb;1"]
+    .getService(Ci.nsIX509CertDB);
+  addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+  pps.registerFilter(new ProxyFilter("https", "localhost", proxy_port, 0), 10);
+
+  initial_session_count = await proxy_session_counter();
+  info(`Initial proxy session count = ${initial_session_count}`);
+});
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("network.http.spdy.enabled");
+  Services.prefs.clearUserPref("network.http.spdy.enabled.http2");
+  Services.prefs.clearUserPref("network.dns.native-is-localhost");
+});
+
+/**
+ * Test series beginning.
+ */
+
+// Check we reach the h2 end server and keep only one session with the proxy for two different origin.
+// Here we use the first isolation token.
+add_task(async function proxy_success_one_session() {
+  proxy_isolation = "TOKEN1";
+
+  const foo = await get_response(make_channel(`https://foo.example.com/random-request-1`));
+  const alt1 = await get_response(make_channel(`https://alt1.example.com/random-request-2`));
+
+  Assert.equal(foo.status, Cr.NS_OK);
+  Assert.equal(foo.http_code, 200);
+  Assert.ok(foo.data.match("random-request-1"));
+  Assert.ok(foo.data.match("You Win!"));
+  Assert.equal(alt1.status, Cr.NS_OK);
+  Assert.equal(alt1.http_code, 200);
+  Assert.ok(alt1.data.match("random-request-2"));
+  Assert.ok(alt1.data.match("You Win!"));
+  Assert.equal(await proxy_session_counter(), 1, "Created just one session with the proxy");
+});
+
+// The proxy responses with 407 instead of 200 Connected, make sure we get a proper error
+// code from the channel and not try to ask for any credentials.
+add_task(async function proxy_auth_failure() {
+  const chan = make_channel(`https://407.example.com/`);
+  const auth_prompt = { triggered: false };
+  chan.notificationCallbacks = new AuthRequestor(() => new UnxpectedAuthPrompt2(auth_prompt));
+  const { status, http_code } = await get_response(chan, CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_PROXY_AUTHENTICATION_FAILED);
+  Assert.equal(http_code, undefined);
+  Assert.equal(auth_prompt.triggered, false, "Auth prompt didn't trigger");
+  Assert.equal(await proxy_session_counter(), 1, "No new session created by 407");
+});
+
+// 502 Bad gateway code returned by the proxy, still one session only, proper different code
+// from the channel.
+add_task(async function proxy_bad_gateway_failure() {
+  const { status, http_code } = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+  Assert.equal(http_code, undefined);
+  Assert.equal(await proxy_session_counter(), 1, "No new session created by 502 after 407");
+});
+
+// Second 502 Bad gateway code returned by the proxy, still one session only with the proxy.
+add_task(async function proxy_bad_gateway_failure_two() {
+  const { status, http_code } = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+  Assert.equal(http_code, undefined);
+  Assert.equal(await proxy_session_counter(), 1, "No new session created by second 502");
+});
+
+// 504 Gateway timeout code returned by the proxy, still one session only, proper different code
+// from the channel.
+add_task(async function proxy_gateway_timeout_failure() {
+  const { status, http_code } = await get_response(make_channel(`https://504.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_PROXY_GATEWAY_TIMEOUT);
+  Assert.equal(http_code, undefined);
+  Assert.equal(await proxy_session_counter(), 1, "No new session created by 504 after 502");
+});
+
+// 404 Not Found means the proxy could not resolve the host.  As for other error responses
+// we still expect this not to close the existing session.
+add_task(async function proxy_host_not_found_failure() {
+  const { status, http_code } = await get_response(make_channel(`https://404.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(status, Cr.NS_ERROR_UNKNOWN_HOST);
+  Assert.equal(http_code, undefined);
+  Assert.equal(await proxy_session_counter(), 1, "No new session created by 404 after 504");
+});
+
+// Make sure that the above error codes don't kill the session and we still reach the end server
+add_task(async function proxy_success_still_one_session() {
+  const foo = await get_response(make_channel(`https://foo.example.com/random-request-1`));
+  const alt1 = await get_response(make_channel(`https://alt1.example.com/random-request-2`));
+
+  Assert.equal(foo.status, Cr.NS_OK);
+  Assert.equal(foo.http_code, 200);
+  Assert.ok(foo.data.match("random-request-1"));
+  Assert.equal(alt1.status, Cr.NS_OK);
+  Assert.equal(alt1.http_code, 200);
+  Assert.ok(alt1.data.match("random-request-2"));
+  Assert.equal(await proxy_session_counter(), 1, "No new session created after proxy error codes");
+});
+
+// Have a new isolation key, this means we are expected to create a new, and again one only,
+// session with the proxy to reach the end server.
+add_task(async function proxy_success_isolated_session() {
+  Assert.notEqual(proxy_isolation, "TOKEN2");
+  proxy_isolation = "TOKEN2";
+
+  const foo = await get_response(make_channel(`https://foo.example.com/random-request-1`));
+  const alt1 = await get_response(make_channel(`https://alt1.example.com/random-request-2`));
+  const lh = await get_response(make_channel(`https://localhost/random-request-3`));
+
+  Assert.equal(foo.status, Cr.NS_OK);
+  Assert.equal(foo.http_code, 200);
+  Assert.ok(foo.data.match("random-request-1"));
+  Assert.ok(foo.data.match("You Win!"));
+  Assert.equal(alt1.status, Cr.NS_OK);
+  Assert.equal(alt1.http_code, 200);
+  Assert.ok(alt1.data.match("random-request-2"));
+  Assert.ok(alt1.data.match("You Win!"));
+  Assert.equal(lh.status, Cr.NS_OK);
+  Assert.equal(lh.http_code, 200);
+  Assert.ok(lh.data.match("random-request-3"));
+  Assert.ok(lh.data.match("You Win!"));
+  Assert.equal(await proxy_session_counter(), 2, "Just one new session seen after changing the isolation key");
+});
+
+// Check that error codes are still handled the same way with new isolation, just in case.
+add_task(async function proxy_bad_gateway_failure_isolated() {
+  const failure1 = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
+  const failure2 = await get_response(make_channel(`https://502.example.com/`), CL_EXPECT_FAILURE);
+
+  Assert.equal(failure1.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+  Assert.equal(failure1.http_code, undefined);
+  Assert.equal(failure2.status, Cr.NS_ERROR_PROXY_BAD_GATEWAY);
+  Assert.equal(failure2.http_code, undefined);
+  Assert.equal(await proxy_session_counter(), 2, "No new session created by 502");
+});
deleted file mode 100644
--- a/netwerk/test/unit/test_proxy-authorization-via-proxyinfo.js
+++ /dev/null
@@ -1,72 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// This test checks that the proxy-auth header is propagated to the CONNECT request when
-// set on a proxy-info object via a proxy filter
-
-const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
-const pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
-
-class TestFilter {
-  constructor(type, host, port, flags, auth) {
-    this._type = type;
-    this._host = host;
-    this._port = port;
-    this._flags = flags;
-    this._auth = auth;
-    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIProtocolProxyFilter]);
-  }
-
-  applyFilter(pps, uri, pi, cb) {
-    cb.onProxyFilterResult(pps.newProxyInfo(
-      this._type, this._host, this._port,
-      this._auth, "", this._flags, 1000, null));
-  }
-};
-
-let httpServer = null;
-let port;
-let connectProcessesed = false;
-const proxyAuthHeader = 'proxy-auth-header-value';
-
-function make_channel(url) {
-  return NetUtil.newChannel({
-    uri: url,
-    loadUsingSystemPrincipal: true
-  });
-}
-
-function connect_handler(request, response)
-{
-  Assert.equal(request.method, "CONNECT");
-  Assert.ok(request.hasHeader("Proxy-Authorization"));
-  Assert.equal(request.getHeader("Proxy-Authorization"), proxyAuthHeader);
-
-  // Just refuse to connect, we have what we need now.
-  response.setStatusLine(request.httpVersion, 500, "STOP");
-  connectProcessesed = true;
-}
-
-function finish_test()
-{
-  Assert.ok(connectProcessesed);
-  httpServer.stop(do_test_finished);
-}
-
-function run_test()
-{
-  httpServer = new HttpServer();
-  httpServer.identity.add("https", "mozilla.org", 443);
-  httpServer.registerPathHandler("CONNECT", connect_handler);
-  httpServer.start(-1);
-  port = httpServer.identity.primaryPort;
-
-  pps.registerFilter(new TestFilter("http", "localhost", port, 0, proxyAuthHeader), 10);
-
-  let chan = make_channel("https://mozilla.org/");
-  chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_FAILURE));
-  do_test_pending();
-}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -446,11 +446,13 @@ skip-if = os == "android"
 [test_network_connectivity_service.js]
 skip-if = os == "android" # DNSv6 issues on android
 [test_suspend_channel_on_authRetry.js]
 [test_suspend_channel_on_examine_merged_response.js]
 [test_bug1527293.js]
 [test_stale-while-revalidate_negative.js]
 [test_stale-while-revalidate_positive.js]
 [test_stale-while-revalidate_max-age-0.js]
-[test_proxy-authorization-via-proxyinfo.js]
-# Bug 1549368
+[test_http1-proxy.js]
+[test_http2-proxy.js]
+run-sequentially = one http2 node proxy is used for all tests, this test is using global session counter
+# so far has to be disabled until we update nodejs to at least 8.4+, then keep off on android (os == "android")
 skip-if = true
--- a/testing/xpcshell/moz-http2/moz-http2.js
+++ b/testing/xpcshell/moz-http2/moz-http2.js
@@ -6,21 +6,29 @@
 // to have node be restarted in between each invocation
 
 var node_http2_root = '../node-http2';
 if (process.env.NODE_HTTP2_ROOT) {
   node_http2_root = process.env.NODE_HTTP2_ROOT;
 }
 var http2 = require(node_http2_root);
 var fs = require('fs');
+var net = require('net');
 var url = require('url');
 var crypto = require('crypto');
 const dnsPacket = require(`${node_http2_root}/../dns-packet`);
 const ip = require(`${node_http2_root}/../node-ip`);
 
+let http2_internal = null;
+try {
+  http2_internal = require('http2');
+} catch (_) {
+  // silently ignored
+}
+
 // Hook into the decompression code to log the decompressed name-value pairs
 var compression_module = node_http2_root + "/lib/protocol/compressor";
 var http2_compression = require(compression_module);
 var HeaderSetDecompressor = http2_compression.HeaderSetDecompressor;
 var originalRead = HeaderSetDecompressor.prototype.read;
 var lastDecompressor;
 var decompressedPairs;
 HeaderSetDecompressor.prototype.read = function() {
@@ -71,16 +79,17 @@ var newTransform = function (frame, enco
   }
   originalTransform.apply(this, arguments);
 };
 
 function getHttpContent(path) {
   var content = '<!doctype html>' +
                 '<html>' +
                 '<head><title>HOORAY!</title></head>' +
+                // 'You Win!' used in tests to check we reached this server
                 '<body>You Win! (by requesting' + path + ')</body>' +
                 '</html>';
   return content;
 }
 
 function generateContent(size) {
   var content = '';
   for (var i = 0; i < size; i++) {
@@ -1120,16 +1129,22 @@ function handleRequest(req, res) {
     res.setHeader('Trailer', 'Server-Timing');
     res.setHeader('Server-Timing', 'metric; dur=123.4; desc=description, metric2; dur=456.78; desc=description1');
     res.write('data reached');
     res.addTrailers({'Server-Timing': 'metric3; dur=789.11; desc=description2, metric4; dur=1112.13; desc=description3'});
     res.end();
     return;
   }
 
+  else if (u.pathname === "/proxy-session-counter") {
+    // Incremented with every newly created session on the proxy
+    res.end(proxy_session_count.toString());
+    return;
+  }
+
   res.setHeader('Content-Type', 'text/html');
   if (req.httpVersionMajor != 2) {
     res.setHeader('Connection', 'close');
   }
   res.writeHead(200);
   res.end(content);
 }
 
@@ -1150,23 +1165,96 @@ var server = http2.createServer(options,
 server.on('connection', function(socket) {
   socket.on('error', function() {
     // Ignoring SSL socket errors, since they usually represent a connection that was tore down
     // by the browser because of an untrusted certificate. And this happens at least once, when
     // the first test case if done.
   });
 });
 
-var serverPort;
-function listenok() {
-  serverPort = server._server.address().port;
-  console.log('HTTP2 server listening on port ' + serverPort);
+
+var proxy = http2_internal ? http2_internal.createSecureServer(options) : null;
+var proxy_session_count = 0;
+
+if (http2_internal) {
+  proxy.on('session', () => {
+    // Can be queried directly on the h2 server under "/proxy-session-counter"
+    ++proxy_session_count;
+  });
+
+  proxy.on('stream', (stream, headers) => {
+    if (headers[':method'] !== 'CONNECT') {
+      // Only accept CONNECT requests
+      stream.close(http2.constants.NGHTTP2_REFUSED_STREAM);
+      return;
+    }
+
+    const target = headers[':authority'];
+
+    const authorization_token = headers['proxy-authorization'];
+    if ('authorization-token' != authorization_token || target == '407.example.com:443') {
+      stream.respond({ ':status': 407 });
+      // Deliberately send no Proxy-Authenticate header
+      stream.end();
+      return;
+    }
+    if (target == '404.example.com:443') {
+      // 404 Not Found, a response code that a proxy should return when the host can't be found
+      stream.respond({ ':status': 404 });
+      stream.end();
+      return;
+    }
+    if (target == '502.example.com:443') {
+      // 502 Bad Gateway, a response code mostly resembling immediate connection error
+      stream.respond({ ':status': 502 });
+      stream.end();
+      return;
+    }
+    if (target == '504.example.com:443') {
+      // 504 Gateway Timeout, did not receive a timely response from an upstream server
+      stream.respond({ ':status': 504 });
+      stream.end();
+      return;
+    }
+
+    const socket = net.connect(serverPort, '127.0.0.1', () => {
+      try {
+        stream.respond({ ':status': 200 });
+        socket.pipe(stream);
+        stream.pipe(socket);
+      } catch (exception) {
+        stream.close(http2.constants.NGHTTP2_STREAM_CLOSED);
+      }
+    });
+    socket.on('error', (error) => {
+      throw `Unxpected error when conneting the HTTP/2 server from the HTTP/2 proxy during CONNECT handling: '${error}'`;
+    });
+  });
 }
-var portSelection = 0;
-var envport = process.env.MOZHTTP2_PORT;
-if (envport !== undefined) {
-  try {
-    portSelection = parseInt(envport, 10);
-  } catch (e) {
-    portSelection = -1;
+
+var serverPort;
+
+const listen = (server, envport) => {
+  if (!server) {
+    return Promise.resolve(0);
   }
+
+  let portSelection = 0;
+  if (envport !== undefined) {
+    try {
+      portSelection = parseInt(envport, 10);
+    } catch (e) {
+      portSelection = -1;
+    }
+  }
+  return new Promise(resolve => {
+    server.listen(portSelection, "0.0.0.0", 200, () => {
+      resolve(server.address().port);
+    });
+  });
 }
-server.listen(portSelection, "0.0.0.0", 200, listenok);
+
+Promise.all([
+  listen(server, process.env.MOZHTTP2_PORT).then(port => serverPort = port),
+  listen(proxy, process.env.MOZHTTP2_PROXY_PORT)
+]).then(([serverPort, proxyPort]) => {
+  console.log(`HTTP2 server listening on ports ${serverPort},${proxyPort}`);
+});
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -1126,19 +1126,21 @@ class XPCShellTests(object):
                 process = Popen([nodeBin, serverJs], stdin=PIPE, stdout=PIPE,
                                 stderr=PIPE, env=self.env, cwd=os.getcwd())
                 self.nodeProc[name] = process
 
                 # Check to make sure the server starts properly by waiting for it to
                 # tell us it's started
                 msg = process.stdout.readline()
                 if 'server listening' in msg:
-                    searchObj = re.search(r'HTTP2 server listening on port (.*)', msg, 0)
+                    searchObj = re.search(r'HTTP2 server listening on ports ([0-9]+),([0-9]+)',
+                                          msg, 0)
                     if searchObj:
                         self.env["MOZHTTP2_PORT"] = searchObj.group(1)
+                        self.env["MOZHTTP2_PROXY_PORT"] = searchObj.group(2)
             except OSError as e:
                 # This occurs if the subprocess couldn't be started
                 self.log.error('Could not run %s server: %s' % (name, str(e)))
                 raise
 
         myDir = os.path.split(os.path.abspath(__file__))[0]
         startServer('moz-http2', os.path.join(myDir, 'moz-http2', 'moz-http2.js'))
 
--- a/xpcom/base/ErrorList.py
+++ b/xpcom/base/ErrorList.py
@@ -324,16 +324,22 @@ with modules["NETWORK"]:
     # banned port.
     errors["NS_ERROR_PORT_ACCESS_NOT_ALLOWED"] = FAILURE(19)
     # The connection was established, but no data was ever received.
     errors["NS_ERROR_NET_RESET"] = FAILURE(20)
     # The connection was established, but the data transfer was interrupted.
     errors["NS_ERROR_NET_INTERRUPT"] = FAILURE(71)
     # The connection attempt to a proxy failed.
     errors["NS_ERROR_PROXY_CONNECTION_REFUSED"] = FAILURE(72)
+    # The proxy requires authentication; used when we can't easily propagate 407s.
+    errors["NS_ERROR_PROXY_AUTHENTICATION_FAILED"] = FAILURE(407)
+    # The proxy failed to connect the remote server.
+    errors["NS_ERROR_PROXY_BAD_GATEWAY"] = FAILURE(502)
+    # The proxy did get any response from the remote server in time.
+    errors["NS_ERROR_PROXY_GATEWAY_TIMEOUT"] = FAILURE(504)
     # A transfer was only partially done when it completed.
     errors["NS_ERROR_NET_PARTIAL_TRANSFER"] = FAILURE(76)
     # HTTP/2 detected invalid TLS configuration
     errors["NS_ERROR_NET_INADEQUATE_SECURITY"] = FAILURE(82)
     # HTTP/2 sent a GOAWAY
     errors["NS_ERROR_NET_HTTP2_SENT_GOAWAY"] = FAILURE(83)
 
     # XXX really need to better rationalize these error codes.  are consumers of