test_httpcancel.js (7495B)
1 // This file ensures that canceling a channel early does not 2 // send the request to the server (bug 350790) 3 // 4 // I've also shoehorned in a test that ENSURE_CALLED_BEFORE_CONNECT works as 5 // expected: see comments that start with ENSURE_CALLED_BEFORE_CONNECT: 6 // 7 // This test also checks that cancelling a channel before asyncOpen, after 8 // onStopRequest, or during onDataAvailable works as expected. 9 10 "use strict"; 11 12 const { HttpServer } = ChromeUtils.importESModule( 13 "resource://testing-common/httpd.sys.mjs" 14 ); 15 const reason = "testing"; 16 17 function inChildProcess() { 18 return Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; 19 } 20 21 var ios = Services.io; 22 var ReferrerInfo = Components.Constructor( 23 "@mozilla.org/referrer-info;1", 24 "nsIReferrerInfo", 25 "init" 26 ); 27 var observer = { 28 QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), 29 30 observe(subject) { 31 subject = subject.QueryInterface(Ci.nsIRequest); 32 subject.cancelWithReason(Cr.NS_BINDING_ABORTED, reason); 33 34 // ENSURE_CALLED_BEFORE_CONNECT: setting values should still work 35 try { 36 subject.QueryInterface(Ci.nsIHttpChannel); 37 let currentReferrer = subject.getRequestHeader("Referer"); 38 Assert.equal(currentReferrer, "http://site1.com/"); 39 var uri = ios.newURI("http://site2.com"); 40 subject.referrerInfo = new ReferrerInfo( 41 Ci.nsIReferrerInfo.EMPTY, 42 true, 43 uri 44 ); 45 } catch (ex) { 46 do_throw("Exception: " + ex); 47 } 48 }, 49 }; 50 51 let cancelDuringOnStartListener = { 52 onStartRequest: function test_onStartR(request) { 53 Assert.equal(request.status, Cr.NS_BINDING_ABORTED); 54 // We didn't sync the reason to child process. 55 if (!inChildProcess()) { 56 Assert.equal(request.canceledReason, reason); 57 } 58 59 // ENSURE_CALLED_BEFORE_CONNECT: setting referrer should now fail 60 try { 61 request.QueryInterface(Ci.nsIHttpChannel); 62 let currentReferrer = request.getRequestHeader("Referer"); 63 Assert.equal(currentReferrer, "http://site2.com/"); 64 var uri = ios.newURI("http://site3.com/"); 65 66 // Need to set NECKO_ERRORS_ARE_FATAL=0 else we'll abort process 67 Services.env.set("NECKO_ERRORS_ARE_FATAL", "0"); 68 // we expect setting referrer to fail 69 try { 70 request.referrerInfo = new ReferrerInfo( 71 Ci.nsIReferrerInfo.EMPTY, 72 true, 73 uri 74 ); 75 do_throw("Error should have been thrown before getting here"); 76 } catch (ex) {} 77 } catch (ex) { 78 do_throw("Exception: " + ex); 79 } 80 }, 81 82 onDataAvailable: function test_ODA() { 83 do_throw("Should not get any data!"); 84 }, 85 86 onStopRequest: function test_onStopR() { 87 this.resolved(); 88 }, 89 }; 90 91 var cancelDuringOnDataListener = { 92 data: "", 93 channel: null, 94 receivedSomeData: null, 95 onStartRequest: function test_onStartR(request) { 96 Assert.equal(request.status, Cr.NS_OK); 97 }, 98 99 onDataAvailable: function test_ODA(request, stream, offset, count) { 100 let string = NetUtil.readInputStreamToString(stream, count); 101 Assert.ok(!string.includes("b")); 102 this.data += string; 103 this.channel.cancel(Cr.NS_BINDING_ABORTED); 104 if (this.receivedSomeData) { 105 this.receivedSomeData(); 106 } 107 }, 108 109 onStopRequest: function test_onStopR(request) { 110 Assert.ok(this.data.includes("a"), `data: ${this.data}`); 111 Assert.equal(request.status, Cr.NS_BINDING_ABORTED); 112 this.resolved(); 113 }, 114 }; 115 116 function makeChan(url) { 117 var chan = NetUtil.newChannel({ 118 uri: url, 119 loadUsingSystemPrincipal: true, 120 }).QueryInterface(Ci.nsIHttpChannel); 121 122 // ENSURE_CALLED_BEFORE_CONNECT: set original value 123 var uri = ios.newURI("http://site1.com"); 124 chan.referrerInfo = new ReferrerInfo(Ci.nsIReferrerInfo.EMPTY, true, uri); 125 return chan; 126 } 127 128 var httpserv = null; 129 130 add_task(async function setup() { 131 httpserv = new HttpServer(); 132 httpserv.registerPathHandler("/failtest", failtest); 133 httpserv.registerPathHandler("/cancel_middle", cancel_middle); 134 httpserv.registerPathHandler("/normal_response", normal_response); 135 httpserv.start(-1); 136 137 registerCleanupFunction(async () => { 138 await new Promise(resolve => httpserv.stop(resolve)); 139 }); 140 }); 141 142 add_task(async function test_cancel_during_onModifyRequest() { 143 var chan = makeChan( 144 "http://localhost:" + httpserv.identity.primaryPort + "/failtest" 145 ); 146 147 if (!inChildProcess()) { 148 Services.obs.addObserver(observer, "http-on-modify-request"); 149 } else { 150 do_send_remote_message("register-observer"); 151 await do_await_remote_message("register-observer-done"); 152 } 153 154 await new Promise(resolve => { 155 cancelDuringOnStartListener.resolved = resolve; 156 chan.asyncOpen(cancelDuringOnStartListener); 157 }); 158 159 if (!inChildProcess()) { 160 Services.obs.removeObserver(observer, "http-on-modify-request"); 161 } else { 162 do_send_remote_message("unregister-observer"); 163 await do_await_remote_message("unregister-observer-done"); 164 } 165 }); 166 167 add_task(async function test_cancel_before_asyncOpen() { 168 var chan = makeChan( 169 "http://localhost:" + httpserv.identity.primaryPort + "/failtest" 170 ); 171 172 chan.cancel(Cr.NS_BINDING_ABORTED); 173 174 Assert.throws( 175 () => { 176 chan.asyncOpen(cancelDuringOnStartListener); 177 }, 178 /NS_BINDING_ABORTED/, 179 "cannot open if already cancelled" 180 ); 181 }); 182 183 add_task(async function test_cancel_during_onData() { 184 var chan = makeChan( 185 "http://localhost:" + httpserv.identity.primaryPort + "/cancel_middle" 186 ); 187 188 await new Promise(resolve => { 189 cancelDuringOnDataListener.resolved = resolve; 190 cancelDuringOnDataListener.channel = chan; 191 chan.asyncOpen(cancelDuringOnDataListener); 192 }); 193 }); 194 195 var cancelAfterOnStopListener = { 196 data: "", 197 channel: null, 198 onStartRequest: function test_onStartR(request) { 199 Assert.equal(request.status, Cr.NS_OK); 200 }, 201 202 onDataAvailable: function test_ODA(request, stream, offset, count) { 203 let string = NetUtil.readInputStreamToString(stream, count); 204 this.data += string; 205 }, 206 207 onStopRequest: function test_onStopR(request) { 208 info("onStopRequest"); 209 Assert.equal(request.status, Cr.NS_OK); 210 this.resolved(); 211 }, 212 }; 213 214 add_task(async function test_cancel_after_onStop() { 215 var chan = makeChan( 216 "http://localhost:" + httpserv.identity.primaryPort + "/normal_response" 217 ); 218 219 await new Promise(resolve => { 220 cancelAfterOnStopListener.resolved = resolve; 221 cancelAfterOnStopListener.channel = chan; 222 chan.asyncOpen(cancelAfterOnStopListener); 223 }); 224 Assert.equal(chan.status, Cr.NS_OK); 225 226 // For now it's unclear if cancelling after onStop should throw, 227 // silently fail, or overwrite the channel's status as we currently do. 228 // See discussion in bug 1553083 229 chan.cancel(Cr.NS_BINDING_ABORTED); 230 Assert.equal(chan.status, Cr.NS_BINDING_ABORTED); 231 }); 232 233 // PATHS 234 235 // /failtest 236 function failtest() { 237 do_throw("This should not be reached"); 238 } 239 240 function cancel_middle(metadata, response) { 241 response.processAsync(); 242 response.setStatusLine(metadata.httpVersion, 200, "OK"); 243 let str1 = "a".repeat(128 * 1024); 244 response.write(str1, str1.length); 245 response.bodyOutputStream.flush(); 246 247 let p = new Promise(resolve => { 248 cancelDuringOnDataListener.receivedSomeData = resolve; 249 }); 250 p.then(() => { 251 let str2 = "b".repeat(128 * 1024); 252 response.write(str2, str2.length); 253 response.finish(); 254 }); 255 } 256 257 function normal_response(metadata, response) { 258 response.setStatusLine(metadata.httpVersion, 200, "OK"); 259 let str1 = "Is this normal?"; 260 response.write(str1, str1.length); 261 }