test_range_requests.js (15506B)
1 // 2 // This test makes sure range-requests are sent and treated the way we want 3 // See bug #612135 for a thorough discussion on the subject 4 // 5 // Necko does a range-request for a partial cache-entry iff 6 // 7 // 1) size of the cached entry < value of the cached Content-Length header 8 // (not tested here - see bug #612135 comments 108-110) 9 // 2) the size of the cached entry is > 0 (see bug #628607) 10 // 3) the cached entry does not have a "no-store" Cache-Control header 11 // 4) the cached entry does not have a Content-Encoding (see bug #613159) 12 // 5) the request does not have a conditional-request header set by client 13 // 6) nsHttpResponseHead::IsResumable() is true for the cached entry 14 // 7) a basic positive test that makes sure byte ranges work 15 // 8) ensure NS_ERROR_CORRUPTED_CONTENT is thrown when total entity size 16 // of 206 does not match content-length of 200 17 // 18 // The test has one handler for each case and run_tests() fires one request 19 // for each. None of the handlers should see a Range-header. 20 21 "use strict"; 22 23 const { HttpServer } = ChromeUtils.importESModule( 24 "resource://testing-common/httpd.sys.mjs" 25 ); 26 27 var httpserver = null; 28 29 const clearTextBody = "This is a slightly longer test\n"; 30 const encodedBody = [ 31 0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, 32 0x74, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, 33 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, 34 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, 0x92, 0xd4, 0xe2, 0x12, 35 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00, 36 ]; 37 38 const partial_data_length = 4; 39 var port = null; // set in run_test 40 41 function make_channel(url) { 42 return NetUtil.newChannel({ 43 uri: url, 44 loadUsingSystemPrincipal: true, 45 }).QueryInterface(Ci.nsIHttpChannel); 46 } 47 48 // StreamListener which cancels its request on first data available 49 function Canceler(continueFn) { 50 this.continueFn = continueFn; 51 } 52 Canceler.prototype = { 53 QueryInterface: ChromeUtils.generateQI([ 54 "nsIStreamListener", 55 "nsIRequestObserver", 56 ]), 57 onStartRequest() {}, 58 59 onDataAvailable(request, stream, offset, count) { 60 // Read stream so we don't assert for not reading from the stream 61 // if cancelling the channel is slow. 62 read_stream(stream, count); 63 64 request.QueryInterface(Ci.nsIChannel).cancel(Cr.NS_BINDING_ABORTED); 65 }, 66 onStopRequest(request, status) { 67 Assert.equal(status, Cr.NS_BINDING_ABORTED); 68 this.continueFn(request, null); 69 }, 70 }; 71 // Simple StreamListener which performs no validations 72 function MyListener(continueFn) { 73 this.continueFn = continueFn; 74 this._buffer = null; 75 } 76 MyListener.prototype = { 77 QueryInterface: ChromeUtils.generateQI([ 78 "nsIStreamListener", 79 "nsIRequestObserver", 80 ]), 81 onStartRequest() { 82 this._buffer = ""; 83 }, 84 85 onDataAvailable(request, stream, offset, count) { 86 this._buffer = this._buffer.concat(read_stream(stream, count)); 87 }, 88 onStopRequest(request) { 89 this.continueFn(request, this._buffer); 90 }, 91 }; 92 93 var case_8_range_request = false; 94 function FailedChannelListener(continueFn) { 95 this.continueFn = continueFn; 96 } 97 FailedChannelListener.prototype = { 98 QueryInterface: ChromeUtils.generateQI([ 99 "nsIStreamListener", 100 "nsIRequestObserver", 101 ]), 102 onStartRequest() {}, 103 104 onDataAvailable(request, stream, offset, count) { 105 read_stream(stream, count); 106 }, 107 108 onStopRequest(request, status) { 109 if (case_8_range_request) { 110 Assert.equal(status, Cr.NS_ERROR_CORRUPTED_CONTENT); 111 } 112 this.continueFn(request, null); 113 }, 114 }; 115 116 function received_cleartext(request, data) { 117 Assert.equal(clearTextBody, data); 118 testFinished(); 119 } 120 121 function setStdHeaders(response, length) { 122 response.setHeader("Content-Type", "text/plain", false); 123 response.setHeader("ETag", "Just testing"); 124 response.setHeader("Cache-Control", "max-age: 360000"); 125 response.setHeader("Accept-Ranges", "bytes"); 126 response.setHeader("Content-Length", "" + length); 127 } 128 129 function handler_2(metadata, response) { 130 setStdHeaders(response, clearTextBody.length); 131 Assert.ok(!metadata.hasHeader("Range")); 132 response.bodyOutputStream.write(clearTextBody, clearTextBody.length); 133 } 134 function received_partial_2(request, data) { 135 Assert.equal(data, undefined); 136 var chan = make_channel("http://localhost:" + port + "/test_2"); 137 chan.asyncOpen(new ChannelListener(received_cleartext, null)); 138 } 139 140 var case_3_request_no = 0; 141 function handler_3(metadata, response) { 142 var body = clearTextBody; 143 setStdHeaders(response, body.length); 144 response.setHeader("Cache-Control", "no-store", false); 145 switch (case_3_request_no) { 146 case 0: 147 Assert.ok(!metadata.hasHeader("Range")); 148 body = body.slice(0, partial_data_length); 149 response.processAsync(); 150 response.bodyOutputStream.write(body, body.length); 151 response.finish(); 152 break; 153 case 1: 154 Assert.ok(!metadata.hasHeader("Range")); 155 response.bodyOutputStream.write(body, body.length); 156 break; 157 default: 158 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 159 } 160 case_3_request_no++; 161 } 162 function received_partial_3(request, data) { 163 Assert.equal(partial_data_length, data.length); 164 var chan = make_channel("http://localhost:" + port + "/test_3"); 165 chan.asyncOpen(new ChannelListener(received_cleartext, null)); 166 } 167 168 var case_4_request_no = 0; 169 function handler_4(metadata, response) { 170 switch (case_4_request_no) { 171 case 0: 172 Assert.ok(!metadata.hasHeader("Range")); 173 var body = encodedBody; 174 setStdHeaders(response, body.length); 175 response.setHeader("Content-Encoding", "gzip", false); 176 body = body.slice(0, partial_data_length); 177 var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( 178 Ci.nsIBinaryOutputStream 179 ); 180 bos.setOutputStream(response.bodyOutputStream); 181 response.processAsync(); 182 bos.writeByteArray(body); 183 response.finish(); 184 break; 185 case 1: 186 Assert.ok(!metadata.hasHeader("Range")); 187 setStdHeaders(response, clearTextBody.length); 188 response.bodyOutputStream.write(clearTextBody, clearTextBody.length); 189 break; 190 default: 191 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 192 } 193 case_4_request_no++; 194 } 195 function received_partial_4() { 196 // checking length does not work with encoded data 197 // do_check_eq(partial_data_length, data.length); 198 var chan = make_channel("http://localhost:" + port + "/test_4"); 199 chan.asyncOpen(new MyListener(received_cleartext)); 200 } 201 202 var case_5_request_no = 0; 203 function handler_5(metadata, response) { 204 var body = clearTextBody; 205 setStdHeaders(response, body.length); 206 switch (case_5_request_no) { 207 case 0: 208 Assert.ok(!metadata.hasHeader("Range")); 209 body = body.slice(0, partial_data_length); 210 response.processAsync(); 211 response.bodyOutputStream.write(body, body.length); 212 response.finish(); 213 break; 214 case 1: 215 Assert.ok(!metadata.hasHeader("Range")); 216 response.bodyOutputStream.write(body, body.length); 217 break; 218 default: 219 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 220 } 221 case_5_request_no++; 222 } 223 function received_partial_5(request, data) { 224 Assert.equal(partial_data_length, data.length); 225 var chan = make_channel("http://localhost:" + port + "/test_5"); 226 chan.setRequestHeader("If-Match", "Some eTag", false); 227 chan.asyncOpen(new ChannelListener(received_cleartext, null)); 228 } 229 230 var case_6_request_no = 0; 231 function handler_6(metadata, response) { 232 switch (case_6_request_no) { 233 case 0: 234 Assert.ok(!metadata.hasHeader("Range")); 235 var body = clearTextBody; 236 setStdHeaders(response, body.length); 237 response.setHeader("Accept-Ranges", "", false); 238 body = body.slice(0, partial_data_length); 239 response.processAsync(); 240 response.bodyOutputStream.write(body, body.length); 241 response.finish(); 242 break; 243 case 1: 244 Assert.ok(!metadata.hasHeader("Range")); 245 setStdHeaders(response, clearTextBody.length); 246 response.bodyOutputStream.write(clearTextBody, clearTextBody.length); 247 break; 248 default: 249 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 250 } 251 case_6_request_no++; 252 } 253 function received_partial_6(request, data) { 254 // would like to verify that the response does not have Accept-Ranges 255 Assert.equal(partial_data_length, data.length); 256 var chan = make_channel("http://localhost:" + port + "/test_6"); 257 chan.asyncOpen(new ChannelListener(received_cleartext, null)); 258 } 259 260 const simpleBody = "0123456789"; 261 262 function received_simple(request, data) { 263 Assert.equal(simpleBody, data); 264 testFinished(); 265 } 266 267 var case_7_request_no = 0; 268 function handler_7(metadata, response) { 269 switch (case_7_request_no) { 270 case 0: 271 Assert.ok(!metadata.hasHeader("Range")); 272 response.setHeader("Content-Type", "text/plain", false); 273 response.setHeader("ETag", "test7Etag"); 274 response.setHeader("Accept-Ranges", "bytes"); 275 response.setHeader("Cache-Control", "max-age=360000"); 276 response.setHeader("Content-Length", "10"); 277 response.processAsync(); 278 response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); 279 response.finish(); 280 break; 281 case 1: 282 response.setHeader("Content-Type", "text/plain", false); 283 response.setHeader("ETag", "test7Etag"); 284 if (metadata.hasHeader("Range")) { 285 Assert.ok(metadata.hasHeader("If-Range")); 286 response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); 287 response.setHeader("Content-Range", "4-9/10"); 288 response.setHeader("Content-Length", "6"); 289 response.bodyOutputStream.write(simpleBody.slice(4), 6); 290 } else { 291 response.setHeader("Content-Length", "10"); 292 response.bodyOutputStream.write(simpleBody, 10); 293 } 294 break; 295 default: 296 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 297 } 298 case_7_request_no++; 299 } 300 function received_partial_7(request, data) { 301 // make sure we get the first 4 bytes 302 Assert.equal(4, data.length); 303 // do it again to get the rest 304 var chan = make_channel("http://localhost:" + port + "/test_7"); 305 chan.asyncOpen(new ChannelListener(received_simple, null)); 306 } 307 308 var case_8_request_no = 0; 309 function handler_8(metadata, response) { 310 switch (case_8_request_no) { 311 case 0: 312 Assert.ok(!metadata.hasHeader("Range")); 313 response.setHeader("Content-Type", "text/plain", false); 314 response.setHeader("ETag", "test8Etag"); 315 response.setHeader("Accept-Ranges", "bytes"); 316 response.setHeader("Cache-Control", "max-age=360000"); 317 response.setHeader("Content-Length", "10"); 318 response.processAsync(); 319 response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); 320 response.finish(); 321 break; 322 case 1: 323 if (metadata.hasHeader("Range")) { 324 Assert.ok(metadata.hasHeader("If-Range")); 325 case_8_range_request = true; 326 } 327 response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); 328 response.setHeader("Content-Type", "text/plain", false); 329 response.setHeader("ETag", "test8Etag"); 330 response.setHeader("Content-Range", "4-8/9"); // intentionally broken 331 response.setHeader("Content-Length", "5"); 332 response.bodyOutputStream.write(simpleBody.slice(4), 5); 333 break; 334 default: 335 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 336 } 337 case_8_request_no++; 338 } 339 function received_partial_8(request, data) { 340 // make sure we get the first 4 bytes 341 Assert.equal(4, data.length); 342 // do it again to get the rest 343 var chan = make_channel("http://localhost:" + port + "/test_8"); 344 chan.asyncOpen( 345 new FailedChannelListener(testFinished, null, CL_EXPECT_LATE_FAILURE) 346 ); 347 } 348 349 var case_9_request_no = 0; 350 function handler_9(metadata, response) { 351 switch (case_9_request_no) { 352 case 0: 353 Assert.ok(!metadata.hasHeader("Range")); 354 response.setHeader("Content-Type", "text/plain", false); 355 response.setHeader("ETag", "W/test9WeakEtag"); 356 response.setHeader("Accept-Ranges", "bytes"); 357 response.setHeader("Cache-Control", "max-age=360000"); 358 response.setHeader("Content-Length", "10"); 359 response.processAsync(); 360 response.bodyOutputStream.write(simpleBody.slice(0, 4), 4); 361 response.finish(); // truncated response 362 break; 363 case 1: 364 Assert.ok(!metadata.hasHeader("Range")); 365 response.setHeader("Content-Type", "text/plain", false); 366 response.setHeader("ETag", "W/test9WeakEtag"); 367 response.setHeader("Accept-Ranges", "bytes"); 368 response.setHeader("Cache-Control", "max-age=360000"); 369 response.setHeader("Content-Length", "10"); 370 response.processAsync(); 371 response.bodyOutputStream.write(simpleBody, 10); 372 response.finish(); // full response 373 break; 374 default: 375 response.setStatusLine(metadata.httpVersion, 404, "Not Found"); 376 } 377 case_9_request_no++; 378 } 379 function received_partial_9(request, data) { 380 Assert.equal(partial_data_length, data.length); 381 var chan = make_channel("http://localhost:" + port + "/test_9"); 382 chan.asyncOpen(new ChannelListener(received_simple, null)); 383 } 384 385 // Simple mechanism to keep track of tests and stop the server 386 var numTestsFinished = 0; 387 function testFinished() { 388 if (++numTestsFinished == 7) { 389 httpserver.stop(do_test_finished); 390 } 391 } 392 393 function run_test() { 394 httpserver = new HttpServer(); 395 httpserver.registerPathHandler("/test_2", handler_2); 396 httpserver.registerPathHandler("/test_3", handler_3); 397 httpserver.registerPathHandler("/test_4", handler_4); 398 httpserver.registerPathHandler("/test_5", handler_5); 399 httpserver.registerPathHandler("/test_6", handler_6); 400 httpserver.registerPathHandler("/test_7", handler_7); 401 httpserver.registerPathHandler("/test_8", handler_8); 402 httpserver.registerPathHandler("/test_9", handler_9); 403 httpserver.start(-1); 404 405 port = httpserver.identity.primaryPort; 406 407 // wipe out cached content 408 evict_cache_entries(); 409 410 // Case 2: zero-length partial entry must not trigger range-request 411 let chan = make_channel("http://localhost:" + port + "/test_2"); 412 chan.asyncOpen(new Canceler(received_partial_2)); 413 414 // Case 3: no-store response must not trigger range-request 415 chan = make_channel("http://localhost:" + port + "/test_3"); 416 chan.asyncOpen(new MyListener(received_partial_3)); 417 418 // Case 4: response with content-encoding must not trigger range-request 419 chan = make_channel("http://localhost:" + port + "/test_4"); 420 chan.asyncOpen(new MyListener(received_partial_4)); 421 422 // Case 5: conditional request-header set by client 423 chan = make_channel("http://localhost:" + port + "/test_5"); 424 chan.asyncOpen(new MyListener(received_partial_5)); 425 426 // Case 6: response is not resumable (drop the Accept-Ranges header) 427 chan = make_channel("http://localhost:" + port + "/test_6"); 428 chan.asyncOpen(new MyListener(received_partial_6)); 429 430 // Case 7: a basic positive test 431 chan = make_channel("http://localhost:" + port + "/test_7"); 432 chan.asyncOpen(new MyListener(received_partial_7)); 433 434 // Case 8: check that mismatched 206 and 200 sizes throw error 435 chan = make_channel("http://localhost:" + port + "/test_8"); 436 chan.asyncOpen(new MyListener(received_partial_8)); 437 438 // Case 9: check that weak etag is not used for a range request 439 chan = make_channel("http://localhost:" + port + "/test_9"); 440 chan.asyncOpen(new MyListener(received_partial_9)); 441 442 do_test_pending(); 443 }