commit 00a66cf54e414c68fd709d5d18be2cf4c07ab87c
parent cdebec5f1e634bccde29011849c4359a30396a0e
Author: Kershaw Chang <kershaw@mozilla.com>
Date: Tue, 14 Oct 2025 08:25:00 +0000
Bug 1993438 - Modernize test_http3.js and extract tests into a separate file, r=necko-reviewers,valentin
Differential Revision: https://phabricator.services.mozilla.com/D268120
Diffstat:
3 files changed, 814 insertions(+), 545 deletions(-)
diff --git a/netwerk/test/unit/http3_common.js b/netwerk/test/unit/http3_common.js
@@ -0,0 +1,719 @@
+/* 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_cache.js */
+/* import-globals-from head_cookies.js */
+/* import-globals-from head_channels.js */
+/* import-globals-from head_http3.js */
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+function makeChan(uri) {
+ let chan = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ }).QueryInterface(Ci.nsIHttpChannel);
+ chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
+ return chan;
+}
+
+// Promise-backed Http3CheckListener
+class Http3CheckListener {
+ constructor(
+ { expectedStatus = Cr.NS_OK, expectedRoute = "" } = {},
+ resolve,
+ reject
+ ) {
+ this.onDataAvailableFired = false;
+ this.expectedStatus = expectedStatus;
+ this.expectedRoute = expectedRoute;
+ this._resolve = resolve;
+ this._reject = reject;
+ }
+
+ onStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.status, this.expectedStatus);
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ Assert.equal(request.responseStatus, 200);
+ }
+ }
+
+ onDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ read_stream(stream, cnt);
+ }
+
+ onStopRequest(request, status) {
+ Assert.equal(status, this.expectedStatus);
+
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump("routed is " + routed + "\n");
+
+ Assert.equal(routed, this.expectedRoute);
+
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ Assert.equal(this.onDataAvailableFired, true);
+ Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3");
+ }
+
+ this._resolve?.(request);
+ }
+}
+
+class WaitForHttp3Listener extends Http3CheckListener {
+ constructor(
+ {
+ expectedStatus = Cr.NS_OK,
+ expectedRoute = "",
+ uri = "",
+ h3AltSvc = "",
+ retry,
+ delayMs = 500,
+ } = {},
+ resolve,
+ reject
+ ) {
+ super({ expectedStatus, expectedRoute }, resolve, reject);
+ this.uri = uri;
+ this.h3AltSvc = h3AltSvc;
+ this._retry = retry; // function to re-open the request
+ this._delayMs = delayMs; // poll interval
+ }
+
+ onStopRequest(request, status) {
+ Assert.equal(status, this.expectedStatus);
+
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump(`routed is ${routed}\n`);
+
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+
+ if (routed === this.expectedRoute) {
+ // (This is where run_next_test() used to be.)
+ Assert.equal(routed, this.expectedRoute); // useful log/assert
+ Assert.equal(httpVersion, "h3");
+ this._resolve?.(request);
+ return;
+ }
+
+ // Not routed yet: mirror old behavior (log + supportsHTTP3 check + retry)
+ dump("poll later for alt-svc mapping\n");
+ if (httpVersion === "h2") {
+ request.QueryInterface(Ci.nsIHttpChannelInternal);
+ Assert.ok(request.supportsHTTP3);
+ }
+
+ if (typeof this._retry === "function") {
+ // schedule another attempt (replaces do_test_pending/do_timeout recursion)
+ do_timeout(this._delayMs, () =>
+ this._retry(this.uri, this.expectedRoute, this.h3AltSvc)
+ );
+ }
+ // Promise remains pending until a later attempt matches expectedRoute.
+ }
+}
+
+// Factory to create { listener, promise }
+function createHttp3CheckListener(options = {}) {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const listener = new Http3CheckListener(options, resolve, reject);
+ return { listener, promise };
+}
+
+function createWaitForHttp3Listener(options = {}) {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const listener = new WaitForHttp3Listener(options, resolve, reject);
+ return { listener, promise };
+}
+
+// --- Async wrapper that does the polling by re-issuing the request ---
+async function waitForHttp3Route(
+ uri,
+ expectedRoute,
+ altSvc,
+ { delayMs = 500 } = {}
+) {
+ let listenerRef;
+
+ // Function to (re)open the channel using the same listener instance.
+ const retry = () => {
+ const chan = makeChan(uri);
+ if (altSvc) {
+ chan.setRequestHeader("x-altsvc", altSvc, false);
+ }
+ chan.asyncOpen(listenerRef);
+ };
+
+ const { listener, promise } = createWaitForHttp3Listener({
+ expectedStatus: Cr.NS_OK,
+ expectedRoute,
+ uri,
+ h3AltSvc: altSvc,
+ retry,
+ delayMs,
+ });
+ listenerRef = listener;
+
+ // Kick off first attempt; subsequent attempts are scheduled by the listener.
+ retry();
+
+ // Resolves only when routed === expectedRoute
+ return promise;
+}
+
+// Promise-backed MultipleListener
+class MultipleListener {
+ constructor(
+ {
+ number_of_parallel_requests = 0,
+ expectedRoute = "",
+ with_error = Cr.NS_OK, // NS_OK means we expect success for all
+ } = {},
+ resolve,
+ reject
+ ) {
+ this.number_of_parallel_requests = number_of_parallel_requests;
+ this.expectedRoute = expectedRoute;
+ this.with_error = with_error;
+
+ this.count_of_done_requests = 0;
+ this.error_found_onstart = false;
+ this.error_found_onstop = false;
+ this.need_cancel_found = false;
+
+ this._resolve = resolve;
+ this._reject = reject;
+ }
+
+ onStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+
+ // Optional cancel behavior via header "CancelMe"
+ let need_cancel = "";
+ try {
+ need_cancel = request.getRequestHeader("CancelMe");
+ } catch (_) {}
+ if (need_cancel !== "") {
+ this.need_cancel_found = true;
+ request.cancel(Cr.NS_ERROR_ABORT);
+ return;
+ }
+
+ // Original logic: either 200 OK for success, or exactly one failure
+ if (Components.isSuccessCode(request.status)) {
+ Assert.equal(request.responseStatus, 200);
+ } else if (this.error_found_onstart) {
+ // Fail fast: more than one failing request on start
+ this._reject?.(
+ new Error("We should have only one request failing (onStart).")
+ );
+ } else {
+ Assert.equal(request.status, this.with_error);
+ this.error_found_onstart = true;
+ }
+ }
+
+ onDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ }
+
+ onStopRequest(request) {
+ // Check Alt-Used routing matches expectation
+ let routed = "";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (_) {}
+ Assert.equal(routed, this.expectedRoute);
+
+ // If success, ensure HTTP/3
+ if (Components.isSuccessCode(request.status)) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (_) {}
+ Assert.equal(httpVersion, "h3");
+ }
+
+ // Track/validate failures (at most one)
+ if (!Components.isSuccessCode(request.status)) {
+ if (this.error_found_onstop) {
+ this._reject?.(
+ new Error("We should have only one request failing (onStop).")
+ );
+ return;
+ }
+ Assert.equal(request.status, this.with_error);
+ this.error_found_onstop = true;
+ }
+
+ // Count completion and maybe resolve
+ this.count_of_done_requests++;
+ if (this.count_of_done_requests === this.number_of_parallel_requests) {
+ if (Components.isSuccessCode(this.with_error)) {
+ // All were expected to succeed
+ Assert.equal(this.error_found_onstart, false);
+ Assert.equal(this.error_found_onstop, false);
+ } else {
+ // One failure was expected OR a cancel path was exercised
+ Assert.ok(this.error_found_onstart || this.need_cancel_found);
+ Assert.equal(this.error_found_onstop, true);
+ }
+ this._resolve?.();
+ }
+ }
+}
+
+// Factory to create { listener, promise }
+function createMultipleListener(options = {}) {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const listener = new MultipleListener(options, resolve, reject);
+ return { listener, promise };
+}
+
+async function do_test_multiple_requests(
+ number_of_parallel_requests,
+ h3Route,
+ httpsOrigin
+) {
+ dump("test_multiple_requests()\n");
+
+ const { listener, promise } = createMultipleListener({
+ number_of_parallel_requests,
+ expectedRoute: h3Route,
+ with_error: Cr.NS_OK,
+ });
+
+ for (let i = 0; i < number_of_parallel_requests; i++) {
+ const chan = makeChan(httpsOrigin + "20000");
+ chan.asyncOpen(listener);
+ }
+
+ await promise;
+}
+
+async function do_test_request_cancelled_by_server(h3Route, httpsOrigin) {
+ dump("do_test_request_cancelled_by_server()\n");
+
+ const { listener, promise } = createHttp3CheckListener({
+ expectedStatus: Cr.NS_ERROR_NET_INTERRUPT,
+ expectedRoute: h3Route,
+ });
+
+ const chan = makeChan(httpsOrigin + "RequestCancelled");
+ chan.asyncOpen(listener);
+
+ // Resolves at the point where run_next_test() used to be called
+ await promise;
+}
+
+// Promise-backed Http3CheckListener must already exist:
+// createHttp3CheckListener({ expectedStatus, expectedRoute })
+
+class CancelRequestListener extends Http3CheckListener {
+ constructor({ expectedRoute = "" } = {}, resolve, reject) {
+ super(
+ { expectedStatus: Cr.NS_ERROR_ABORT, expectedRoute },
+ resolve,
+ reject
+ );
+ }
+
+ onStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(Components.isSuccessCode(request.status), true);
+ // Cancel the request immediately (simulate Necko cancelling)
+ request.cancel(Cr.NS_ERROR_ABORT);
+ }
+}
+
+function createCancelRequestListener(options = {}) {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const listener = new CancelRequestListener(options, resolve, reject);
+ return { listener, promise };
+}
+
+// Cancel stream after OnStartRequest.
+async function do_test_stream_cancelled_by_necko(h3Route, httpsOrigin) {
+ dump("do_test_stream_cancelled_by_necko()\n");
+
+ const { listener, promise } = createCancelRequestListener({
+ expectedRoute: h3Route,
+ });
+
+ const chan = makeChan(httpsOrigin + "20000");
+ chan.asyncOpen(listener);
+
+ // Resolves at the end of onStopRequest (where run_next_test() used to be)
+ await promise;
+}
+
+async function do_test_multiple_request_one_is_cancelled(
+ number_of_parallel_requests,
+ h3Route,
+ httpsOrigin
+) {
+ dump("do_test_multiple_request_one_is_cancelled()\n");
+
+ const { listener, promise } = createMultipleListener({
+ number_of_parallel_requests,
+ expectedRoute: h3Route,
+ with_error: Cr.NS_ERROR_NET_INTERRUPT, // one request is expected to fail (server-cancelled)
+ });
+
+ for (let i = 0; i < number_of_parallel_requests; i++) {
+ let uri = httpsOrigin + "20000";
+ if (i === 4) {
+ // Add a request that will be cancelled by the server.
+ uri = httpsOrigin + "RequestCancelled";
+ }
+ const chan = makeChan(uri);
+ chan.asyncOpen(listener);
+ }
+
+ // Resolves when all parallel requests complete and invariants are checked
+ await promise;
+}
+
+async function do_test_multiple_request_one_is_cancelled_by_necko(
+ number_of_parallel_requests,
+ h3Route,
+ httpsOrigin
+) {
+ dump("do_test_multiple_request_one_is_cancelled_by_necko()\n");
+
+ const { listener, promise } = createMultipleListener({
+ number_of_parallel_requests,
+ expectedRoute: h3Route,
+ with_error: Cr.NS_ERROR_ABORT,
+ });
+
+ for (let i = 0; i < number_of_parallel_requests; i++) {
+ let chan = makeChan(httpsOrigin + "20000");
+ if (i === 4) {
+ // MultipleListener will cancel request with this header.
+ chan.setRequestHeader("CancelMe", "true", false);
+ }
+ chan.asyncOpen(listener);
+ }
+
+ // Resolves when all parallel requests complete and invariants are checked
+ await promise;
+}
+
+// Promise-backed Http3CheckListener assumed available:
+// function createHttp3CheckListener({ expectedStatus, expectedRoute })
+
+class PostListener extends Http3CheckListener {
+ constructor(opts = {}, resolve, reject) {
+ super(opts, resolve, reject);
+ }
+ onDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ read_stream(stream, cnt);
+ }
+}
+
+// Factory for PostListener
+function createPostListener(options = {}) {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const listener = new PostListener(options, resolve, reject);
+ return { listener, promise };
+}
+
+// Helper to perform a POST (or any method with a body)
+function openWithBody(
+ content,
+ chan,
+ method = "POST",
+ contentType = "text/plain"
+) {
+ const stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stream.setByteStringData(content);
+
+ const uchan = chan.QueryInterface(Ci.nsIUploadChannel);
+ uchan.setUploadStream(stream, contentType, stream.available());
+ chan.requestMethod = method;
+ return chan;
+}
+
+// Generate a post with known pre-calculated md5 sum.
+function generateContent(size) {
+ let content = "";
+ for (let i = 0; i < size; i++) {
+ content += "0";
+ }
+ return content;
+}
+
+let post = generateContent(10);
+
+// Test a simple POST (async)
+async function do_test_post(httpsOrigin, h3Route) {
+ dump("do_test_post()\n");
+
+ const chan = makeChan(httpsOrigin + "post");
+ openWithBody(post, chan, "POST");
+
+ const { listener, promise } = createPostListener({
+ expectedStatus: Cr.NS_OK,
+ expectedRoute: h3Route,
+ });
+
+ chan.asyncOpen(listener);
+ await promise; // resolves at end of onStopRequest in Http3CheckListener
+}
+
+// Test a simple PATCH
+async function do_test_patch(httpsOrigin, h3Route) {
+ dump("do_test_post()\n");
+
+ const chan = makeChan(httpsOrigin + "patch");
+ openWithBody(post, chan, "PATCH");
+
+ const { listener, promise } = createPostListener({
+ expectedStatus: Cr.NS_OK,
+ expectedRoute: h3Route,
+ });
+
+ chan.asyncOpen(listener);
+ await promise;
+}
+
+let h1Server = null;
+let altsvcHost = "";
+let httpOrigin = "";
+
+function h1Response(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "text/plain", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ try {
+ let hval = "h3=" + metadata.getHeader("x-altsvc");
+ response.setHeader("Alt-Svc", hval, false);
+ } catch (e) {}
+
+ let body = "Q: What did 0 say to 8? A: Nice Belt!\n";
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function h1ServerWK(metadata, response) {
+ response.setStatusLine(metadata.httpVersion, 200, "OK");
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Connection", "close", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Access-Control-Allow-Method", "GET", false);
+ response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
+
+ let body = `["http://${altsvcHost}:${h1Server.identity.primaryPort}"]`;
+ response.bodyOutputStream.write(body, body.length);
+}
+
+function setup_h1_server(host) {
+ altsvcHost = host;
+ h1Server = new HttpServer();
+ h1Server.registerPathHandler("/http3-test", h1Response);
+ h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
+ h1Server.registerPathHandler("/VersionFallback", h1Response);
+ h1Server.start(-1);
+ h1Server.identity.setPrimary(
+ "http",
+ altsvcHost,
+ h1Server.identity.primaryPort
+ );
+ httpOrigin = `http://${altsvcHost}:${h1Server.identity.primaryPort}/`;
+ registerCleanupFunction(() => {
+ h1Server.stop();
+ });
+}
+
+// Promise-backed base assumed available:
+// class Http3CheckListener { ... }
+// function createHttp3CheckListener(opts) { return { listener, promise }; }
+
+class SlowReceiverListener extends Http3CheckListener {
+ constructor(
+ {
+ expectedStatus = Cr.NS_OK,
+ expectedRoute = "",
+ expectedBytes = 10_000_000,
+ } = {},
+ resolve,
+ reject
+ ) {
+ super({ expectedStatus, expectedRoute }, resolve, reject);
+ this.count = 0;
+ this.expectedBytes = expectedBytes;
+ }
+
+ onDataAvailable(request, stream, off, cnt) {
+ this.onDataAvailableFired = true;
+ this.count += cnt;
+ read_stream(stream, cnt);
+ }
+
+ onStopRequest(request, status) {
+ Assert.equal(status, this.expectedStatus);
+ Assert.equal(this.count, this.expectedBytes);
+
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump(`routed is ${routed}\n`);
+ Assert.equal(routed, this.expectedRoute);
+
+ if (Components.isSuccessCode(this.expectedStatus)) {
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "h3");
+ Assert.equal(this.onDataAvailableFired, true);
+ }
+
+ // Resolve where run_next_test() used to be
+ this._resolve?.(request);
+ }
+}
+
+function createSlowReceiverListener(options = {}) {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const listener = new SlowReceiverListener(options, resolve, reject);
+ return { listener, promise };
+}
+
+// Test: slow receiver (suspend, then resume)
+async function do_test_slow_receiver(httpsOrigin, h3Route) {
+ dump("do_test_slow_receiver()\n");
+
+ const chan = makeChan(httpsOrigin + "10000000");
+
+ const { listener, promise } = createSlowReceiverListener({
+ expectedStatus: Cr.NS_OK,
+ expectedRoute: h3Route,
+ expectedBytes: 10_000_000,
+ });
+
+ chan.asyncOpen(listener);
+
+ // Suspend immediately, then resume after 1s (replaces do_test_pending/do_timeout)
+ chan.suspend();
+ await new Promise(r => do_timeout(1000, r));
+ chan.resume();
+
+ // Wait for completion (used to be run_next_test/do_test_finished)
+ await promise;
+}
+
+// Promise-backed listener for version fallback checks
+class CheckFallbackListener {
+ constructor(resolve, reject) {
+ this._resolve = resolve;
+ this._reject = reject;
+ }
+
+ onStartRequest(request) {
+ Assert.ok(request instanceof Ci.nsIHttpChannel);
+ Assert.equal(request.status, Cr.NS_OK);
+ Assert.equal(request.responseStatus, 200);
+ }
+
+ onDataAvailable(request, stream, off, cnt) {
+ read_stream(stream, cnt);
+ }
+
+ onStopRequest(request, status) {
+ Assert.equal(status, Cr.NS_OK);
+
+ let routed = "NA";
+ try {
+ routed = request.getRequestHeader("Alt-Used");
+ } catch (e) {}
+ dump(`routed is ${routed}\n`);
+ Assert.equal(routed, "0");
+
+ let httpVersion = "";
+ try {
+ httpVersion = request.protocolVersion;
+ } catch (e) {}
+ Assert.equal(httpVersion, "http/1.1");
+
+ // Resolve where run_next_test() used to be called
+ this._resolve?.(request);
+ }
+}
+
+// Factory to create { listener, promise }
+function createCheckFallbackListener() {
+ let resolve, reject;
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ const listener = new CheckFallbackListener(resolve, reject);
+ return { listener, promise };
+}
+
+// Server cancels request with VersionFallback.
+async function do_test_version_fallback(httpsOrigin) {
+ dump("do_test_version_fallback()\n");
+
+ const chan = makeChan(httpsOrigin + "VersionFallback");
+ const { listener, promise } = createCheckFallbackListener();
+
+ chan.asyncOpen(listener);
+
+ await promise;
+}
diff --git a/netwerk/test/unit/test_http3.js b/netwerk/test/unit/test_http3.js
@@ -1,64 +1,18 @@
"use strict";
-const { HttpServer } = ChromeUtils.importESModule(
- "resource://testing-common/httpd.sys.mjs"
-);
-
-// Generate a post with known pre-calculated md5 sum.
-function generateContent(size) {
- let content = "";
- for (let i = 0; i < size; i++) {
- content += "0";
- }
- return content;
-}
-
-let post = generateContent(10);
+/* import-globals-from http3_common.js */
// Max concurent stream number in neqo is 100.
// Openning 120 streams will test queuing of streams.
let number_of_parallel_requests = 120;
-let h1Server = null;
let h3Route;
let httpsOrigin;
-let httpOrigin;
let h3AltSvc;
let h3Port;
-
+let h3ServerDomain;
let prefs;
-let tests = [
- // This test must be the first because it setsup alt-svc connection, that
- // other tests use.
- test_https_alt_svc,
- test_multiple_requests,
- test_request_cancelled_by_server,
- test_stream_cancelled_by_necko,
- test_multiple_request_one_is_cancelled,
- test_multiple_request_one_is_cancelled_by_necko,
- test_post,
- test_patch,
- test_http_alt_svc,
- test_slow_receiver,
- // This test should be at the end, because it will close http3
- // connection and the transaction will switch to already existing http2
- // connection.
- // TODO: Bug 1582667 should try to fix issue with connection being closed.
- test_version_fallback,
- testsDone,
-];
-
-let current_test = 0;
-
-function run_next_test() {
- if (current_test < tests.length) {
- dump("starting test number " + current_test + "\n");
- tests[current_test]();
- current_test++;
- }
-}
-
-function run_test() {
+add_setup(async function () {
let h2Port = Services.env.get("MOZHTTP2_PORT");
Assert.notEqual(h2Port, null);
Assert.notEqual(h2Port, "");
@@ -66,185 +20,21 @@ function run_test() {
Assert.notEqual(h3Port, null);
Assert.notEqual(h3Port, "");
h3AltSvc = ":" + h3Port;
-
- h3Route = "foo.example.com:" + h3Port;
+ h3ServerDomain = "foo.example.com";
+ h3Route = `${h3ServerDomain}:${h3Port}`;
do_get_profile();
prefs = Services.prefs;
prefs.setBoolPref("network.http.http3.enable", true);
- prefs.setCharPref("network.dns.localDomains", "foo.example.com");
+ prefs.setCharPref(
+ "network.dns.localDomains",
+ "foo.example.com, alt1.example.com"
+ );
// We always resolve elements of localDomains as it's hardcoded without the
// following pref:
prefs.setBoolPref("network.proxy.allow_hijacking_localhost", true);
prefs.setBoolPref("network.http.altsvc.oe", true);
- // The certificate for the http3server server 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");
- httpsOrigin = "https://foo.example.com:" + h2Port + "/";
-
- h1Server = new HttpServer();
- h1Server.registerPathHandler("/http3-test", h1Response);
- h1Server.registerPathHandler("/.well-known/http-opportunistic", h1ServerWK);
- h1Server.registerPathHandler("/VersionFallback", h1Response);
- h1Server.start(-1);
- h1Server.identity.setPrimary(
- "http",
- "foo.example.com",
- h1Server.identity.primaryPort
- );
- httpOrigin = "http://foo.example.com:" + h1Server.identity.primaryPort + "/";
-
- run_next_test();
-}
-
-function h1Response(metadata, response) {
- response.setStatusLine(metadata.httpVersion, 200, "OK");
- response.setHeader("Content-Type", "text/plain", false);
- response.setHeader("Connection", "close", false);
- response.setHeader("Cache-Control", "no-cache", false);
- response.setHeader("Access-Control-Allow-Origin", "*", false);
- response.setHeader("Access-Control-Allow-Method", "GET", false);
- response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
-
- try {
- let hval = "h3=" + metadata.getHeader("x-altsvc");
- response.setHeader("Alt-Svc", hval, false);
- } catch (e) {}
-
- let body = "Q: What did 0 say to 8? A: Nice Belt!\n";
- response.bodyOutputStream.write(body, body.length);
-}
-
-function h1ServerWK(metadata, response) {
- response.setStatusLine(metadata.httpVersion, 200, "OK");
- response.setHeader("Content-Type", "application/json", false);
- response.setHeader("Connection", "close", false);
- response.setHeader("Cache-Control", "no-cache", false);
- response.setHeader("Access-Control-Allow-Origin", "*", false);
- response.setHeader("Access-Control-Allow-Method", "GET", false);
- response.setHeader("Access-Control-Allow-Headers", "x-altsvc", false);
-
- let body = '["http://foo.example.com:' + h1Server.identity.primaryPort + '"]';
- response.bodyOutputStream.write(body, body.length);
-}
-
-function makeChan(uri) {
- let chan = NetUtil.newChannel({
- uri,
- loadUsingSystemPrincipal: true,
- }).QueryInterface(Ci.nsIHttpChannel);
- chan.loadFlags = Ci.nsIChannel.LOAD_INITIAL_DOCUMENT_URI;
- return chan;
-}
-
-let Http3CheckListener = function () {};
-
-Http3CheckListener.prototype = {
- onDataAvailableFired: false,
- expectedStatus: Cr.NS_OK,
- expectedRoute: "",
-
- onStartRequest: function testOnStartRequest(request) {
- Assert.ok(request instanceof Ci.nsIHttpChannel);
-
- Assert.equal(request.status, this.expectedStatus);
- if (Components.isSuccessCode(this.expectedStatus)) {
- Assert.equal(request.responseStatus, 200);
- }
- },
-
- onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
- this.onDataAvailableFired = true;
- read_stream(stream, cnt);
- },
-
- onStopRequest: function testOnStopRequest(request, status) {
- Assert.equal(status, this.expectedStatus);
- let routed = "NA";
- try {
- routed = request.getRequestHeader("Alt-Used");
- } catch (e) {}
- dump("routed is " + routed + "\n");
-
- Assert.equal(routed, this.expectedRoute);
-
- if (Components.isSuccessCode(this.expectedStatus)) {
- let httpVersion = "";
- try {
- httpVersion = request.protocolVersion;
- } catch (e) {}
- Assert.equal(httpVersion, "h3");
- Assert.equal(this.onDataAvailableFired, true);
- Assert.equal(request.getResponseHeader("X-Firefox-Http3"), "h3");
- }
- run_next_test();
- do_test_finished();
- },
-};
-
-let WaitForHttp3Listener = function () {};
-
-WaitForHttp3Listener.prototype = new Http3CheckListener();
-
-WaitForHttp3Listener.prototype.uri = "";
-WaitForHttp3Listener.prototype.h3AltSvc = "";
-
-WaitForHttp3Listener.prototype.onStopRequest = function testOnStopRequest(
- request,
- status
-) {
- Assert.equal(status, this.expectedStatus);
-
- let routed = "NA";
- try {
- routed = request.getRequestHeader("Alt-Used");
- } catch (e) {}
- dump("routed is " + routed + "\n");
-
- let httpVersion = "";
- try {
- httpVersion = request.protocolVersion;
- } catch (e) {}
-
- if (routed == this.expectedRoute) {
- Assert.equal(routed, this.expectedRoute); // always true, but a useful log
- Assert.equal(httpVersion, "h3");
- run_next_test();
- } else {
- dump("poll later for alt svc mapping\n");
- if (httpVersion == "h2") {
- request.QueryInterface(Ci.nsIHttpChannelInternal);
- Assert.ok(request.supportsHTTP3);
- }
- do_test_pending();
- do_timeout(500, () => {
- doTest(this.uri, this.expectedRoute, this.h3AltSvc);
- });
- }
-
- do_test_finished();
-};
-
-function doTest(uri, expectedRoute, altSvc) {
- let chan = makeChan(uri);
- let listener = new WaitForHttp3Listener();
- listener.uri = uri;
- listener.expectedRoute = expectedRoute;
- listener.h3AltSvc = altSvc;
- chan.setRequestHeader("x-altsvc", altSvc, false);
- chan.asyncOpen(listener);
-}
-
-// Test Alt-Svc for http3.
-// H2 server returns alt-svc=h3=:h3port
-function test_https_alt_svc() {
- dump("test_https_alt_svc()\n");
- do_test_pending();
if (mozinfo.os == "android") {
// Set necessary prefs to make Firefox connect to the http3Server on the
// host machine.
@@ -252,341 +42,99 @@ function test_https_alt_svc() {
const overrideService = Cc[
"@mozilla.org/network/native-dns-override;1"
].getService(Ci.nsINativeDNSResolverOverride);
- overrideService.addIPOverride("foo.example.com", "10.0.2.2");
+ overrideService.addIPOverride(h3ServerDomain, "10.0.2.2");
prefs.setCharPref(
"network.http.http3.alt-svc-mapping-for-testing",
- `foo.example.com;h3=:${h3Port}`
+ `${h3ServerDomain};h3=:${h3Port}`
);
}
- doTest(httpsOrigin + "http3-test", h3Route, h3AltSvc);
-}
-
-// Listener for a number of parallel requests. if with_error is set, one of
-// the channels will be cancelled (by the server or in onStartRequest).
-let MultipleListener = function () {};
-
-MultipleListener.prototype = {
- number_of_parallel_requests: 0,
- with_error: Cr.NS_OK,
- count_of_done_requests: 0,
- error_found_onstart: false,
- error_found_onstop: false,
- need_cancel_found: false,
-
- onStartRequest: function testOnStartRequest(request) {
- Assert.ok(request instanceof Ci.nsIHttpChannel);
- let need_cancel = "";
- try {
- need_cancel = request.getRequestHeader("CancelMe");
- } catch (e) {}
- if (need_cancel != "") {
- this.need_cancel_found = true;
- request.cancel(Cr.NS_ERROR_ABORT);
- } else if (Components.isSuccessCode(request.status)) {
- Assert.equal(request.responseStatus, 200);
- } else if (this.error_found_onstart) {
- do_throw("We should have only one request failing.");
- } else {
- Assert.equal(request.status, this.with_error);
- this.error_found_onstart = true;
- }
- },
-
- onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
- read_stream(stream, cnt);
- },
-
- onStopRequest: function testOnStopRequest(request) {
- let routed = "";
- try {
- routed = request.getRequestHeader("Alt-Used");
- } catch (e) {}
- Assert.equal(routed, this.expectedRoute);
-
- if (Components.isSuccessCode(request.status)) {
- let httpVersion = "";
- try {
- httpVersion = request.protocolVersion;
- } catch (e) {}
- Assert.equal(httpVersion, "h3");
- }
-
- if (!Components.isSuccessCode(request.status)) {
- if (this.error_found_onstop) {
- do_throw("We should have only one request failing.");
- } else {
- Assert.equal(request.status, this.with_error);
- this.error_found_onstop = true;
- }
- }
- this.count_of_done_requests++;
- if (this.count_of_done_requests == this.number_of_parallel_requests) {
- if (Components.isSuccessCode(this.with_error)) {
- Assert.equal(this.error_found_onstart, false);
- Assert.equal(this.error_found_onstop, false);
- } else {
- Assert.ok(this.error_found_onstart || this.need_cancel_found);
- Assert.equal(this.error_found_onstop, true);
- }
- run_next_test();
- }
- do_test_finished();
- },
-};
-
-// Multiple requests
-function test_multiple_requests() {
- dump("test_multiple_requests()\n");
-
- let listener = new MultipleListener();
- listener.number_of_parallel_requests = number_of_parallel_requests;
- listener.expectedRoute = h3Route;
-
- for (let i = 0; i < number_of_parallel_requests; i++) {
- let chan = makeChan(httpsOrigin + "20000");
- chan.asyncOpen(listener);
- do_test_pending();
- }
-}
-
-// A request cancelled by a server.
-function test_request_cancelled_by_server() {
- dump("test_request_cancelled_by_server()\n");
-
- let listener = new Http3CheckListener();
- listener.expectedStatus = Cr.NS_ERROR_NET_INTERRUPT;
- listener.expectedRoute = h3Route;
- let chan = makeChan(httpsOrigin + "RequestCancelled");
- chan.asyncOpen(listener);
- do_test_pending();
-}
-
-let CancelRequestListener = function () {};
-
-CancelRequestListener.prototype = new Http3CheckListener();
-
-CancelRequestListener.prototype.expectedStatus = Cr.NS_ERROR_ABORT;
-
-CancelRequestListener.prototype.onStartRequest = function testOnStartRequest(
- request
-) {
- Assert.ok(request instanceof Ci.nsIHttpChannel);
-
- Assert.equal(Components.isSuccessCode(request.status), true);
- request.cancel(Cr.NS_ERROR_ABORT);
-};
-
-// Cancel stream after OnStartRequest.
-function test_stream_cancelled_by_necko() {
- dump("test_stream_cancelled_by_necko()\n");
-
- let listener = new CancelRequestListener();
- listener.expectedRoute = h3Route;
- let chan = makeChan(httpsOrigin + "20000");
- chan.asyncOpen(listener);
- do_test_pending();
-}
-
-// Multiple requests, one gets cancelled by the server, the other should finish normally.
-function test_multiple_request_one_is_cancelled() {
- dump("test_multiple_request_one_is_cancelled()\n");
-
- let listener = new MultipleListener();
- listener.number_of_parallel_requests = number_of_parallel_requests;
- listener.with_error = Cr.NS_ERROR_NET_INTERRUPT;
- listener.expectedRoute = h3Route;
-
- for (let i = 0; i < number_of_parallel_requests; i++) {
- let uri = httpsOrigin + "20000";
- if (i == 4) {
- // Add a request that will be cancelled by the server.
- uri = httpsOrigin + "RequestCancelled";
- }
- let chan = makeChan(uri);
- chan.asyncOpen(listener);
- do_test_pending();
- }
-}
-
-// Multiple requests, one gets cancelled by us, the other should finish normally.
-function test_multiple_request_one_is_cancelled_by_necko() {
- dump("test_multiple_request_one_is_cancelled_by_necko()\n");
-
- let listener = new MultipleListener();
- listener.number_of_parallel_requests = number_of_parallel_requests;
- listener.with_error = Cr.NS_ERROR_ABORT;
- listener.expectedRoute = h3Route;
- for (let i = 0; i < number_of_parallel_requests; i++) {
- let chan = makeChan(httpsOrigin + "20000");
- if (i == 4) {
- // MultipleListener will cancel request with this header.
- chan.setRequestHeader("CancelMe", "true", false);
- }
- chan.asyncOpen(listener);
- do_test_pending();
- }
-}
-
-let PostListener = function () {};
-
-PostListener.prototype = new Http3CheckListener();
-
-PostListener.prototype.onDataAvailable = function (request, stream, off, cnt) {
- this.onDataAvailableFired = true;
- read_stream(stream, cnt);
-};
-
-// Support for doing a POST
-function do_post(content, chan, listener, method) {
- let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
- Ci.nsIStringInputStream
+ // The certificate for the http3server server 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
);
- stream.setByteStringData(content);
-
- let uchan = chan.QueryInterface(Ci.nsIUploadChannel);
- uchan.setUploadStream(stream, "text/plain", stream.available());
-
- chan.requestMethod = method;
-
- chan.asyncOpen(listener);
-}
-
-// Test a simple POST
-function test_post() {
- dump("test_post()");
- let chan = makeChan(httpsOrigin + "post");
- let listener = new PostListener();
- listener.expectedRoute = h3Route;
- do_post(post, chan, listener, "POST");
- do_test_pending();
-}
-
-// Test a simple PATCH
-function test_patch() {
- dump("test_patch()");
- let chan = makeChan(httpsOrigin + "patch");
- let listener = new PostListener();
- listener.expectedRoute = h3Route;
- do_post(post, chan, listener, "PATCH");
- do_test_pending();
-}
-
-// Test alt-svc for http (without s)
-function test_http_alt_svc() {
- dump("test_http_alt_svc()\n");
- // Skip this test on Android because the httpOrigin (http://foo.example.com)
- // is on 127.0.0.1, while the http3Server (https://foo.example.com) is
- // on 10.0.2.2. Currently, we can't change the IP mapping dynamically.
- if (mozinfo.os == "android") {
- current_test++;
- run_next_test();
- return;
- }
- do_test_pending();
- doTest(httpOrigin + "http3-test", h3Route, h3AltSvc);
-}
-
-let SlowReceiverListener = function () {};
-
-SlowReceiverListener.prototype = new Http3CheckListener();
-SlowReceiverListener.prototype.count = 0;
-
-SlowReceiverListener.prototype.onDataAvailable = function (
- request,
- stream,
- off,
- cnt
-) {
- this.onDataAvailableFired = true;
- this.count += cnt;
- read_stream(stream, cnt);
-};
-
-SlowReceiverListener.prototype.onStopRequest = function (request, status) {
- Assert.equal(status, this.expectedStatus);
- Assert.equal(this.count, 10000000);
- let routed = "NA";
- try {
- routed = request.getRequestHeader("Alt-Used");
- } catch (e) {}
- dump("routed is " + routed + "\n");
-
- Assert.equal(routed, this.expectedRoute);
-
- if (Components.isSuccessCode(this.expectedStatus)) {
- let httpVersion = "";
- try {
- httpVersion = request.protocolVersion;
- } catch (e) {}
- Assert.equal(httpVersion, "h3");
- Assert.equal(this.onDataAvailableFired, true);
- }
- run_next_test();
- do_test_finished();
-};
-
-function test_slow_receiver() {
- dump("test_slow_receiver()\n");
- let chan = makeChan(httpsOrigin + "10000000");
- let listener = new SlowReceiverListener();
- listener.expectedRoute = h3Route;
- chan.asyncOpen(listener);
- do_test_pending();
- chan.suspend();
- do_timeout(1000, chan.resume);
-}
-
-let CheckFallbackListener = function () {};
-
-CheckFallbackListener.prototype = {
- onStartRequest: function testOnStartRequest(request) {
- Assert.ok(request instanceof Ci.nsIHttpChannel);
-
- Assert.equal(request.status, Cr.NS_OK);
- Assert.equal(request.responseStatus, 200);
- },
+ addCertFromFile(certdb, "http2-ca.pem", "CTu,u,u");
+ httpsOrigin = `https://${h3ServerDomain}:${h2Port}/`;
+
+ registerCleanupFunction(() => {
+ prefs.clearUserPref("network.http.http3.enable");
+ prefs.clearUserPref("network.dns.localDomains");
+ prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
+ prefs.clearUserPref("network.http.altsvc.oe");
+ });
+});
+
+add_task(async function test_https_alt_svc() {
+ await waitForHttp3Route(httpsOrigin + "http3-test", h3Route, h3AltSvc, {
+ delayMs: 500,
+ });
+});
+
+add_task(async function test_multiple_requests() {
+ await do_test_multiple_requests(
+ number_of_parallel_requests,
+ h3Route,
+ httpsOrigin
+ );
+});
- onDataAvailable: function testOnDataAvailable(request, stream, off, cnt) {
- read_stream(stream, cnt);
- },
+add_task(async function test_request_cancelled_by_server() {
+ await do_test_request_cancelled_by_server(h3Route, httpsOrigin);
+});
- onStopRequest: function testOnStopRequest(request, status) {
- Assert.equal(status, Cr.NS_OK);
- let routed = "NA";
- try {
- routed = request.getRequestHeader("Alt-Used");
- } catch (e) {}
- dump("routed is " + routed + "\n");
+add_task(async function test_stream_cancelled_by_necko() {
+ await do_test_stream_cancelled_by_necko(h3Route, httpsOrigin);
+});
- Assert.equal(routed, "0");
+add_task(async function test_multiple_request_one_is_cancelled() {
+ await do_test_multiple_request_one_is_cancelled(
+ number_of_parallel_requests,
+ h3Route,
+ httpsOrigin
+ );
+});
- let httpVersion = "";
- try {
- httpVersion = request.protocolVersion;
- } catch (e) {}
- Assert.equal(httpVersion, "http/1.1");
- run_next_test();
- do_test_finished();
+add_task(async function test_multiple_request_one_is_cancelled_by_necko() {
+ await do_test_multiple_request_one_is_cancelled_by_necko(
+ number_of_parallel_requests,
+ h3Route,
+ httpsOrigin
+ );
+});
+
+add_task(async function test_post() {
+ await do_test_post(httpsOrigin, h3Route);
+});
+
+add_task(async function test_patch() {
+ await do_test_patch(httpsOrigin, h3Route);
+});
+
+add_task(
+ {
+ // Skip this test on Android because the httpOrigin (http://foo.example.com)
+ // is on 127.0.0.1, while the http3Server (https://foo.example.com) is
+ // on 10.0.2.2. Currently, we can't change the IP mapping dynamically.
+ skip_if: () => mozinfo.os == "android",
},
-};
-
-// Server cancels request with VersionFallback.
-function test_version_fallback() {
- dump("test_version_fallback()\n");
-
- let chan = makeChan(httpsOrigin + "VersionFallback");
- let listener = new CheckFallbackListener();
- chan.asyncOpen(listener);
- do_test_pending();
-}
+ async function test_http_alt_svc() {
+ setup_h1_server(h3ServerDomain);
+ await waitForHttp3Route(httpOrigin + "http3-test", h3Route, h3AltSvc, {
+ delayMs: 500,
+ });
+ }
+);
-function testsDone() {
- prefs.clearUserPref("network.http.http3.enable");
- prefs.clearUserPref("network.dns.localDomains");
- prefs.clearUserPref("network.proxy.allow_hijacking_localhost");
- prefs.clearUserPref("network.http.altsvc.oe");
- dump("testDone\n");
- do_test_pending();
- h1Server.stop(do_test_finished);
-}
+add_task(async function test_slow_receiver() {
+ await do_test_slow_receiver(httpsOrigin, h3Route);
+});
+
+// This test should be at the end, because it will close http3
+// connection and the transaction will switch to already existing http2
+// connection.
+// TODO: Bug 1582667 should try to fix issue with connection being closed.
+add_task(async function test_version_fallback() {
+ await do_test_version_fallback(httpsOrigin);
+});
diff --git a/netwerk/test/unit/xpcshell.toml b/netwerk/test/unit/xpcshell.toml
@@ -27,6 +27,7 @@ support-files = [
"test_http3_prio_helpers.js",
"http2_test_common.js",
"http3_proxy_common.js",
+ "http3_common.js",
]
# dom.serviceWorkers.enabled is currently set to false in StaticPrefList.yaml
@@ -709,6 +710,7 @@ run-sequentially = ["true"] # node server exceptions dont replay well
head = "head_channels.js head_cache.js head_cookies.js head_trr.js head_http3.js http2_test_common.js"
["test_http3.js"]
+head = "head_cookies.js head_channels.js head_cache.js head_http3.js http3_common.js"
run-sequentially = ["true"] # http3server
skip-if = [
"os == 'android' && os_version == '14' && processor == 'x86_64'", # Bug 1982955