// This file ensures that canceling a channel early does not // send the request to the server (bug 350790) // // I've also shoehorned in a test that ENSURE_CALLED_BEFORE_CONNECT works as // expected: see comments that start with ENSURE_CALLED_BEFORE_CONNECT: // // This test also checks that cancelling a channel before asyncOpen, after // onStopRequest, or during onDataAvailable works as expected. "use strict"; const { HttpServer } = ChromeUtils.importESModule( "resource://testing-common/httpd.sys.mjs" ); const reason = "testing"; function inChildProcess() { return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; } var ios = Services.io; var ReferrerInfo = Components.Constructor( "@mozilla.org/referrer-info;1", "nsIReferrerInfo", "init" ); var observer = { QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), observe(subject) { subject = subject.QueryInterface(Ci.nsIRequest); subject.cancelWithReason(Cr.NS_BINDING_ABORTED, reason); // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work try { subject.QueryInterface(Ci.nsIHttpChannel); let currentReferrer = subject.getRequestHeader("Referer"); Assert.equal(currentReferrer, "http://site1.com/"); var uri = ios.newURI("http://site2.com"); subject.referrerInfo = new ReferrerInfo( Ci.nsIReferrerInfo.EMPTY, true, uri ); } catch (ex) { do_throw("Exception: " + ex); } }, }; let cancelDuringOnStartListener = { onStartRequest: function test_onStartR(request) { Assert.equal(request.status, Cr.NS_BINDING_ABORTED); // We didn't sync the reason to child process. if (!inChildProcess()) { Assert.equal(request.canceledReason, reason); } // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail try { request.QueryInterface(Ci.nsIHttpChannel); let currentReferrer = request.getRequestHeader("Referer"); Assert.equal(currentReferrer, "http://site2.com/"); var uri = ios.newURI("http://site3.com/"); // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process Services.env.set("NECKO_ERRORS_ARE_FATAL", "0"); // we expect setting referrer to fail try { request.referrerInfo = new ReferrerInfo( Ci.nsIReferrerInfo.EMPTY, true, uri ); do_throw("Error should have been thrown before getting here"); } catch (ex) {} } catch (ex) { do_throw("Exception: " + ex); } }, onDataAvailable: function test_ODA() { do_throw("Should not get any data!"); }, onStopRequest: function test_onStopR() { this.resolved(); }, }; var cancelDuringOnDataListener = { data: "", channel: null, receivedSomeData: null, onStartRequest: function test_onStartR(request) { Assert.equal(request.status, Cr.NS_OK); }, onDataAvailable: function test_ODA(request, stream, offset, count) { let string = NetUtil.readInputStreamToString(stream, count); Assert.ok(!string.includes("b")); this.data += string; this.channel.cancel(Cr.NS_BINDING_ABORTED); if (this.receivedSomeData) { this.receivedSomeData(); } }, onStopRequest: function test_onStopR(request) { Assert.ok(this.data.includes("a"), `data: ${this.data}`); Assert.equal(request.status, Cr.NS_BINDING_ABORTED); this.resolved(); }, }; function makeChan(url) { var chan = NetUtil.newChannel({ uri: url, loadUsingSystemPrincipal: true, }).QueryInterface(Ci.nsIHttpChannel); // ENSURE_CALLED_BEFORE_CONNECT: set original value var uri = ios.newURI("http://site1.com"); chan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); return chan; } var httpserv = null; add_task(async function setup() { httpserv = new HttpServer(); httpserv.registerPathHandler("/failtest", failtest); httpserv.registerPathHandler("/cancel_middle", cancel_middle); httpserv.registerPathHandler("/normal_response", normal_response); httpserv.start(-1); registerCleanupFunction(async () => { await new Promise(resolve => httpserv.stop(resolve)); }); }); add_task(async function test_cancel_during_onModifyRequest() { var chan = makeChan( "http://localhost:" + httpserv.identity.primaryPort + "/failtest" ); if (!inChildProcess()) { Services.obs.addObserver(observer, "http-on-modify-request"); } else { do_send_remote_message("register-observer"); await do_await_remote_message("register-observer-done"); } await new Promise(resolve => { cancelDuringOnStartListener.resolved = resolve; chan.asyncOpen(cancelDuringOnStartListener); }); if (!inChildProcess()) { Services.obs.removeObserver(observer, "http-on-modify-request"); } else { do_send_remote_message("unregister-observer"); await do_await_remote_message("unregister-observer-done"); } }); add_task(async function test_cancel_before_asyncOpen() { var chan = makeChan( "http://localhost:" + httpserv.identity.primaryPort + "/failtest" ); chan.cancel(Cr.NS_BINDING_ABORTED); Assert.throws( () => { chan.asyncOpen(cancelDuringOnStartListener); }, /NS_BINDING_ABORTED/, "cannot open if already cancelled" ); }); add_task(async function test_cancel_during_onData() { var chan = makeChan( "http://localhost:" + httpserv.identity.primaryPort + "/cancel_middle" ); await new Promise(resolve => { cancelDuringOnDataListener.resolved = resolve; cancelDuringOnDataListener.channel = chan; chan.asyncOpen(cancelDuringOnDataListener); }); }); var cancelAfterOnStopListener = { data: "", channel: null, onStartRequest: function test_onStartR(request) { Assert.equal(request.status, Cr.NS_OK); }, onDataAvailable: function test_ODA(request, stream, offset, count) { let string = NetUtil.readInputStreamToString(stream, count); this.data += string; }, onStopRequest: function test_onStopR(request) { info("onStopRequest"); Assert.equal(request.status, Cr.NS_OK); this.resolved(); }, }; add_task(async function test_cancel_after_onStop() { var chan = makeChan( "http://localhost:" + httpserv.identity.primaryPort + "/normal_response" ); await new Promise(resolve => { cancelAfterOnStopListener.resolved = resolve; cancelAfterOnStopListener.channel = chan; chan.asyncOpen(cancelAfterOnStopListener); }); Assert.equal(chan.status, Cr.NS_OK); // For now it's unclear if cancelling after onStop should throw, // silently fail, or overwrite the channel's status as we currently do. // See discussion in bug 1553083 chan.cancel(Cr.NS_BINDING_ABORTED); Assert.equal(chan.status, Cr.NS_BINDING_ABORTED); }); // PATHS // /failtest function failtest() { do_throw("This should not be reached"); } function cancel_middle(metadata, response) { response.processAsync(); response.setStatusLine(metadata.httpVersion, 200, "OK"); let str1 = "a".repeat(128 * 1024); response.write(str1, str1.length); response.bodyOutputStream.flush(); let p = new Promise(resolve => { cancelDuringOnDataListener.receivedSomeData = resolve; }); p.then(() => { let str2 = "b".repeat(128 * 1024); response.write(str2, str2.length); response.finish(); }); } function normal_response(metadata, response) { response.setStatusLine(metadata.httpVersion, 200, "OK"); let str1 = "Is this normal?"; response.write(str1, str1.length); }