commit bc019c8286148bb97827a47694b893cffcc74dc1
parent a6cb564e827269dd0bfd19900012955cec40d63a
Author: Valentin Gosu <valentin.gosu@gmail.com>
Date: Fri, 14 Nov 2025 11:08:45 +0000
Bug 1999128 - Make HTTP3 transactions that fail to activate fallback to HTTP2 right away r=necko-reviewers,kershaw
In this case the transactions fail to be dispatched because the
headers contain a non-ascii character and parse_headers in neqo_glue
uses str::from_utf8 to parse the header buffer.
When this happens, instead of returning NS_OK and waiting for the
H3 connection to potentially time out, we return NS_ERROR_HTTP2_FALLBACK_TO_HTTP1
to force the transaction to be dispatched to a TCP connection.
Differential Revision: https://phabricator.services.mozilla.com/D272223
Diffstat:
4 files changed, 102 insertions(+), 3 deletions(-)
diff --git a/netwerk/protocol/http/Http3Session.cpp b/netwerk/protocol/http/Http3Session.cpp
@@ -1503,8 +1503,14 @@ nsresult Http3Session::TryActivating(
QueueStream(aStream);
return rv;
}
+ if (rv == NS_ERROR_DOM_INVALID_HEADER_VALUE) {
+ // neqo_http3conn_fetch may fail if the headers contain non-ascii
+ // values. In that case we want to fallback to HTTP/2 right away.
+ // HACK: This should be removed when we fix it in bug 1999659
+ return NS_ERROR_HTTP2_FALLBACK_TO_HTTP1;
+ }
+
// Ignore this error. This may happen if some events are not handled yet.
- // TODO we may try to add an assertion here.
return NS_OK;
}
diff --git a/netwerk/socket/neqo_glue/src/lib.rs b/netwerk/socket/neqo_glue/src/lib.rs
@@ -45,7 +45,7 @@ use nserror::{
NS_ERROR_FILE_ALREADY_EXISTS, NS_ERROR_ILLEGAL_VALUE, NS_ERROR_INVALID_ARG,
NS_ERROR_NET_HTTP3_PROTOCOL_ERROR, NS_ERROR_NET_INTERRUPT, NS_ERROR_NET_RESET,
NS_ERROR_NET_TIMEOUT, NS_ERROR_NOT_AVAILABLE, NS_ERROR_NOT_CONNECTED, NS_ERROR_OUT_OF_MEMORY,
- NS_ERROR_SOCKET_ADDRESS_IN_USE, NS_ERROR_UNEXPECTED, NS_OK,
+ NS_ERROR_SOCKET_ADDRESS_IN_USE, NS_ERROR_UNEXPECTED, NS_OK, NS_ERROR_DOM_INVALID_HEADER_VALUE,
};
use nsstring::{nsACString, nsCString};
use thin_vec::ThinVec;
@@ -1146,7 +1146,9 @@ fn parse_headers(headers: &nsACString) -> Result<Vec<Header>, nsresult> {
// They need to be split into (String, String) pairs.
match str::from_utf8(headers) {
Err(_) => {
- return Err(NS_ERROR_INVALID_ARG);
+ // Header names are checked for UTF8 in Necko
+ // Values aren't necessaryly UTF-8 - Should be fixed in bug 1999659
+ return Err(NS_ERROR_DOM_INVALID_HEADER_VALUE);
}
Ok(h) => {
for elem in h.split("\r\n").skip(1) {
diff --git a/netwerk/test/unit/test_http3_non_ascii_header.js b/netwerk/test/unit/test_http3_non_ascii_header.js
@@ -0,0 +1,87 @@
+/* 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/. */
+
+"use strict";
+
+/* import-globals-from head_http3.js */
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+function channelOpenPromise(chan, flags) {
+ return new Promise(resolve => {
+ function finish(req, buffer) {
+ resolve([req, buffer]);
+ }
+ chan.asyncOpen(new ChannelListener(finish, null, flags));
+ });
+}
+
+let h2Port;
+let h3Port;
+
+add_setup(async function setup() {
+ h2Port = Services.env.get("MOZHTTP2_PORT");
+ Assert.notEqual(h2Port, null);
+ Assert.notEqual(h2Port, "");
+
+ h3Port = Services.env.get("MOZHTTP3_PORT");
+ Assert.notEqual(h3Port, null);
+ Assert.notEqual(h3Port, "");
+
+ Services.prefs.setBoolPref("network.http.http3.enable", true);
+ Services.prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ Services.prefs.setBoolPref("network.dns.disableIPv6", true);
+ Services.prefs.setCharPref(
+ "network.http.http3.alt-svc-mapping-for-testing",
+ `foo.example.com;h3=:${h3Port}`
+ );
+
+ let certdb = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+
+ let chan = makeChan(`https://localhost`);
+ await channelOpenPromise(chan, CL_EXPECT_FAILURE);
+});
+
+// Test non-ASCII header with HTTP/3 and HTTP/2 fallback
+// Http3Session::TryActivating fails to parse the non-utf8 headers
+// and returns NS_ERROR_HTTP2_FALLBACK_TO_HTTP1 so we fallback to h2.
+// When bug 1999659 is fixed we should instead succeed.
+add_task(async function test_non_ascii_header() {
+ // First request with non-ASCII header
+ let chan1 = makeChan(`https://foo.example.com:${h2Port}/100`);
+ chan1.setRequestHeader("x-panel-title", "ä", false);
+
+ let [req1, buf1] = await channelOpenPromise(
+ chan1,
+ CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
+ );
+
+ let protocol1 = req1.protocolVersion;
+ Assert.ok(protocol1 === "h3" || protocol1 === "h2", `Using ${protocol1}`);
+ Assert.equal(req1.responseStatus, 200);
+ info(buf1);
+
+ // Second request with different non-ASCII header
+ let chan2 = makeChan(`https://foo.example.com:${h2Port}/100`);
+ chan2.setRequestHeader("x-panel-title", "ö", false);
+
+ let [req2] = await channelOpenPromise(
+ chan2,
+ CL_IGNORE_CL | CL_ALLOW_UNKNOWN_CL
+ );
+
+ let protocol2 = req2.protocolVersion;
+ Assert.ok(protocol2 === "h3" || protocol2 === "h2", `Using ${protocol2}`);
+ Assert.equal(req2.responseStatus, 200);
+});
diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml
@@ -850,6 +850,10 @@ skip-if = [
"os == 'win' && os_version == '11.26100' && processor == 'x86_64'",
]
+["test_http3_non_ascii_header.js"]
+head = "head_cookies.js head_channels.js head_cache.js head_http3.js"
+run-sequentially = ["true"] # http3server
+
["test_http3_perf.js"]
run-sequentially = ["true"] # http3server
skip-if = [