From 7453b6a04cf4f27fb5d63860a8c8f62e9b40fb89 Mon Sep 17 00:00:00 2001 From: "bjarne@runitsoft.com" Date: Fri, 4 Feb 2011 14:49:05 -0500 Subject: [PATCH] Bug 612135: Content Encoding Error (partial content request) on galaxys.samsungmobile.com r=bzbarsky a=blocking2.0 --- netwerk/protocol/http/nsHttpChannel.cpp | 27 +- netwerk/test/unit/test_gzipped_206.js | 60 ++-- netwerk/test/unit/test_range_requests.js | 275 +++++++++++++++++++ netwerk/test/unit/test_resumable_truncate.js | 8 - 4 files changed, 318 insertions(+), 52 deletions(-) create mode 100644 netwerk/test/unit/test_range_requests.js diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp index 683b8ba0a792..f3f724337e51 100644 --- a/netwerk/protocol/http/nsHttpChannel.cpp +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -2486,6 +2486,13 @@ nsHttpChannel::CheckCache() PRUint16 isCachedRedirect = mCachedResponseHead->Status()/100 == 3; + mCustomConditionalRequest = + mRequestHead.PeekHeader(nsHttp::If_Modified_Since) || + mRequestHead.PeekHeader(nsHttp::If_None_Match) || + mRequestHead.PeekHeader(nsHttp::If_Unmodified_Since) || + mRequestHead.PeekHeader(nsHttp::If_Match) || + mRequestHead.PeekHeader(nsHttp::If_Range); + if (method != nsHttp::Head && !isCachedRedirect) { // If the cached content-length is set and it does not match the data // size of the cached content, then the cached response is partial... @@ -2500,8 +2507,17 @@ nsHttpChannel::CheckCache() if (nsInt64(size) != contentLength) { LOG(("Cached data size does not match the Content-Length header " "[content-length=%lld size=%u]\n", PRInt64(contentLength), size)); - if ((nsInt64(size) < contentLength) && mCachedResponseHead->IsResumable()) { - // looks like a partial entry. + + PRBool hasContentEncoding = + mCachedResponseHead->PeekHeader(nsHttp::Content_Encoding) + != nsnull; + if ((nsInt64(size) < contentLength) && + size > 0 && + !hasContentEncoding && + mCachedResponseHead->IsResumable() && + !mCustomConditionalRequest && + !mCachedResponseHead->NoStore()) { + // looks like a partial entry we can reuse rv = SetupByteRangeRequest(size); NS_ENSURE_SUCCESS(rv, rv); mCachedContentIsPartial = PR_TRUE; @@ -2514,13 +2530,6 @@ nsHttpChannel::CheckCache() PRBool doValidation = PR_FALSE; PRBool canAddImsHeader = PR_TRUE; - mCustomConditionalRequest = - mRequestHead.PeekHeader(nsHttp::If_Modified_Since) || - mRequestHead.PeekHeader(nsHttp::If_None_Match) || - mRequestHead.PeekHeader(nsHttp::If_Unmodified_Since) || - mRequestHead.PeekHeader(nsHttp::If_Match) || - mRequestHead.PeekHeader(nsHttp::If_Range); - // If the LOAD_FROM_CACHE flag is set, any cached data can simply be used. if (mLoadFlags & LOAD_FROM_CACHE) { LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n")); diff --git a/netwerk/test/unit/test_gzipped_206.js b/netwerk/test/unit/test_gzipped_206.js index 9e3b65c07138..5a483efb0fed 100644 --- a/netwerk/test/unit/test_gzipped_206.js +++ b/netwerk/test/unit/test_gzipped_206.js @@ -2,9 +2,18 @@ do_load_httpd_js(); var httpserver = null; -const responseBody = [0x1f, 0x8b, 0x08, 0x00, 0x16, 0x5a, 0x8a, 0x48, 0x02, - 0x03, 0x2b, 0x49, 0x2d, 0x2e, 0xe1, 0x02, 0x00, 0xc6, - 0x35, 0xb9, 0x3b, 0x05, 0x00, 0x00, 0x00]; +// testString = "This is a slightly longer test\n"; +const responseBody = [0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, 0x74, 0x66, 0x69, + 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, + 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, + 0x92, 0xd4, 0xe2, 0x12, 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00]; + +function getCacheService() +{ + var nsCacheService = Components.classes["@mozilla.org/network/cache-service;1"]; + var service = nsCacheService.getService(Components.interfaces.nsICacheService); + return service; +} function make_channel(url, callback, ctx) { var ios = Cc["@mozilla.org/network/io-service;1"]. @@ -18,6 +27,8 @@ function cachedHandler(metadata, response) { response.setHeader("Content-Type", "application/x-gzip", false); response.setHeader("Content-Encoding", "gzip", false); response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age=3600000"); // avoid validation + response.setHeader("Content-Length", "" + responseBody.length); var body = responseBody; @@ -37,6 +48,7 @@ function cachedHandler(metadata, response) { response.setHeader("Content-Range", from + "-" + to + "/" + responseBody.length, false); } else { response.setHeader("Accept-Ranges", "bytes"); + body = body.slice(0, 17); // slice off a piece to send first doRangeResponse = true; } @@ -44,49 +56,24 @@ function cachedHandler(metadata, response) { .createInstance(Ci.nsIBinaryOutputStream); bos.setOutputStream(response.bodyOutputStream); + response.processAsync(); bos.writeByteArray(body, body.length); + response.finish(); } -function Canceler() { -} - -Canceler.prototype = { - QueryInterface: function(iid) { - if (iid.equals(Ci.nsIStreamListener) || - iid.equals(Ci.nsIRequestObserver) || - iid.equals(Ci.nsISupports)) - return this; - throw Components.results.NS_ERROR_NO_INTERFACE; - }, - - onStartRequest: function(request, context) { - }, - - onDataAvailable: function(request, context, stream, offset, count) { - request.QueryInterface(Ci.nsIChannel) - .cancel(Components.results.NS_BINDING_ABORTED); - }, - - onStopRequest: function(request, context, status) { - do_check_eq(status, Components.results.NS_BINDING_ABORTED); - continue_test(); - } -}; - -function continue_test() { +function continue_test(request, data) { + do_check_true(17 == data.length); var chan = make_channel("http://localhost:4444/cached/test.gz"); - chan.asyncOpen(new ChannelListener(finish_test, null), null); + chan.asyncOpen(new ChannelListener(finish_test, null, CL_EXPECT_GZIP), null); } function finish_test(request, data, ctx) { - do_test_pending(); - httpserver.stop(do_test_finished); do_check_eq(request.status, 0); do_check_eq(data.length, responseBody.length); for (var i = 0; i < data.length; ++i) { do_check_eq(data.charCodeAt(i), responseBody[i]); } - do_test_finished(); + httpserver.stop(do_test_finished); } function run_test() { @@ -94,7 +81,10 @@ function run_test() { httpserver.registerPathHandler("/cached/test.gz", cachedHandler); httpserver.start(4444); + // wipe out cached content + getCacheService().evictEntries(Components.interfaces.nsICache.STORE_ANYWHERE); + var chan = make_channel("http://localhost:4444/cached/test.gz"); - chan.asyncOpen(new Canceler(), null); + chan.asyncOpen(new ChannelListener(continue_test, null, CL_EXPECT_GZIP), null); do_test_pending(); } diff --git a/netwerk/test/unit/test_range_requests.js b/netwerk/test/unit/test_range_requests.js new file mode 100644 index 000000000000..03af7f232fdd --- /dev/null +++ b/netwerk/test/unit/test_range_requests.js @@ -0,0 +1,275 @@ +// +// This test makes sure range-requests are sent and treated the way we want +// See bug #612135 for a thorough discussion on the subject +// +// Necko does a range-request for a partial cache-entry iff +// +// 1) size of the cached entry < value of the cached Content-Length header +// (not tested here - see bug #612135 comments 108-110) +// 2) the size of the cached entry is > 0 (see bug #628607) +// 3) the cached entry does not have a "no-store" Cache-Control header +// 4) the cached entry does not have a Content-Encoding (see bug #613159) +// 5) the request does not have a conditional-request header set by client +// 6) nsHttpResponseHead::IsResumable() is true for the cached entry +// +// The test has one handler for each case and run_tests() fires one request +// for each. None of the handlers should see a Range-header. + +do_load_httpd_js(); + +var httpserver = null; + +const clearTextBody = "This is a slightly longer test\n"; +const encodedBody = [0x1f, 0x8b, 0x08, 0x08, 0xef, 0x70, 0xe6, 0x4c, 0x00, 0x03, 0x74, 0x65, 0x78, 0x74, 0x66, 0x69, + 0x6c, 0x65, 0x2e, 0x74, 0x78, 0x74, 0x00, 0x0b, 0xc9, 0xc8, 0x2c, 0x56, 0x00, 0xa2, 0x44, 0x85, + 0xe2, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0x9c, 0x4a, 0x85, 0x9c, 0xfc, 0xbc, 0xf4, 0xd4, 0x22, 0x85, + 0x92, 0xd4, 0xe2, 0x12, 0x2e, 0x2e, 0x00, 0x00, 0xe5, 0xe6, 0xf0, 0x20, 0x00, 0x00, 0x00]; +const decodedBody = [0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x6c, 0x69, 0x67, 0x68, 0x74, + 0x6c, 0x79, 0x20, 0x6c, 0x6f, 0x6e, 0x67, 0x65, 0x72, 0x20, 0x74, 0x65, 0x73, 0x74, 0x0a, 0x0a]; + +const partial_data_length = 4; + +function getCacheService() +{ + var nsCacheService = Components.classes["@mozilla.org/network/cache-service;1"]; + var service = nsCacheService.getService(Components.interfaces.nsICacheService); + return service; +} + +function make_channel(url, callback, ctx) { + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + var chan = ios.newChannel(url, "", null); + return chan.QueryInterface(Ci.nsIHttpChannel); +} + +// StreamListener which cancels its request on first data available +function Canceler(continueFn) { + this.continueFn = continueFn; +} +Canceler.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onStartRequest: function(request, context) { }, + + onDataAvailable: function(request, context, stream, offset, count) { + request.QueryInterface(Ci.nsIChannel) + .cancel(Components.results.NS_BINDING_ABORTED); + }, + onStopRequest: function(request, context, status) { + do_check_eq(status, Components.results.NS_BINDING_ABORTED); + this.continueFn(request, null); + } +}; +// Simple StreamListener which performs no validations +function MyListener(continueFn) { + this.continueFn = continueFn; + this._buffer = null; +} +MyListener.prototype = { + QueryInterface: function(iid) { + if (iid.equals(Ci.nsIStreamListener) || + iid.equals(Ci.nsIRequestObserver) || + iid.equals(Ci.nsISupports)) + return this; + throw Components.results.NS_ERROR_NO_INTERFACE; + }, + onStartRequest: function(request, context) { this._buffer = ""; }, + + onDataAvailable: function(request, context, stream, offset, count) { + this._buffer = this._buffer.concat(read_stream(stream, count)); + }, + onStopRequest: function(request, context, status) { + this.continueFn(request, this._buffer); + } +}; + +function received_cleartext(request, data) { + do_check_eq(clearTextBody, data); + testFinished(); +} + +function setStdHeaders(response, length) { + response.setHeader("Content-Type", "text/plain", false); + response.setHeader("ETag", "Just testing"); + response.setHeader("Cache-Control", "max-age: 360000"); + response.setHeader("Accept-Ranges", "bytes"); + response.setHeader("Content-Length", "" + length); +} + +function handler_2(metadata, response) { + setStdHeaders(response, clearTextBody.length); + do_check_false(metadata.hasHeader("Range")); + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); +} +function received_partial_2(request, data) { + do_check_eq(data, undefined); + var chan = make_channel("http://localhost:4444/test_2"); + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); +} + +var case_3_request_no = 0; +function handler_3(metadata, response) { + var body = clearTextBody; + setStdHeaders(response, body.length); + response.setHeader("Cache-Control", "no-store", false); + switch (case_3_request_no) { + case 0: + do_check_false(metadata.hasHeader("Range")); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + do_check_false(metadata.hasHeader("Range")); + response.bodyOutputStream.write(body, body.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_3_request_no++; +} +function received_partial_3(request, data) { + do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:4444/test_3"); + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); +} + +var case_4_request_no = 0; +function handler_4(metadata, response) { + switch (case_4_request_no) { + case 0: + do_check_false(metadata.hasHeader("Range")); + var body = encodedBody; + setStdHeaders(response, body.length); + response.setHeader("Content-Encoding", "gzip", false); + body = body.slice(0, partial_data_length); + var bos = Cc["@mozilla.org/binaryoutputstream;1"] + .createInstance(Ci.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + response.processAsync(); + bos.writeByteArray(body, body.length); + response.finish(); + break; + case 1: + do_check_false(metadata.hasHeader("Range")); + setStdHeaders(response, clearTextBody.length); + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_4_request_no++; +} +function received_partial_4(request, data) { +// checking length does not work with encoded data +// do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:4444/test_4"); + chan.asyncOpen(new MyListener(received_cleartext), null); +} + +var case_5_request_no = 0; +function handler_5(metadata, response) { + var body = clearTextBody; + setStdHeaders(response, body.length); + switch (case_5_request_no) { + case 0: + do_check_false(metadata.hasHeader("Range")); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + do_check_false(metadata.hasHeader("Range")); + response.bodyOutputStream.write(body, body.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_5_request_no++; +} +function received_partial_5(request, data) { + do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:4444/test_5"); + chan.setRequestHeader("If-Match", "Some eTag", false); + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); +} + +var case_6_request_no = 0; +function handler_6(metadata, response) { + switch (case_6_request_no) { + case 0: + do_check_false(metadata.hasHeader("Range")); + var body = clearTextBody; + setStdHeaders(response, body.length); + response.setHeader("Accept-Ranges", "", false); + body = body.slice(0, partial_data_length); + response.processAsync(); + response.bodyOutputStream.write(body, body.length); + response.finish(); + break; + case 1: + do_check_false(metadata.hasHeader("Range")); + setStdHeaders(response, clearTextBody.length); + response.bodyOutputStream.write(clearTextBody, clearTextBody.length); + break; + default: + response.setStatusLine(metadata.httpVersion, 404, "Not Found"); + } + case_6_request_no++; +} +function received_partial_6(request, data) { +// would like to verify that the response does not have Accept-Ranges + do_check_eq(partial_data_length, data.length); + var chan = make_channel("http://localhost:4444/test_6"); + chan.asyncOpen(new ChannelListener(received_cleartext, null), null); +} + +// Simple mechanism to keep track of tests and stop the server +var numTestsFinished = 0; +function testFinished() { + if (++numTestsFinished == 5) + httpserver.stop(do_test_finished); +} + +function run_test() { + httpserver = new nsHttpServer(); + httpserver.registerPathHandler("/test_2", handler_2); + httpserver.registerPathHandler("/test_3", handler_3); + httpserver.registerPathHandler("/test_4", handler_4); + httpserver.registerPathHandler("/test_5", handler_5); + httpserver.registerPathHandler("/test_6", handler_6); + httpserver.start(4444); + + // wipe out cached content + getCacheService().evictEntries(Components.interfaces.nsICache.STORE_ANYWHERE); + + // Case 2: zero-length partial entry must not trigger range-request + var chan = make_channel("http://localhost:4444/test_2"); + chan.asyncOpen(new Canceler(received_partial_2), null); + + // Case 3: no-store response must not trigger range-request + var chan = make_channel("http://localhost:4444/test_3"); + chan.asyncOpen(new MyListener(received_partial_3), null); + + // Case 4: response with content-encoding must not trigger range-request + var chan = make_channel("http://localhost:4444/test_4"); + chan.asyncOpen(new MyListener(received_partial_4), null); + + // Case 5: conditional request-header set by client + var chan = make_channel("http://localhost:4444/test_5"); + chan.asyncOpen(new MyListener(received_partial_5), null); + + // Case 6: response is not resumable (drop the Accept-Ranges header) + var chan = make_channel("http://localhost:4444/test_6"); + chan.asyncOpen(new MyListener(received_partial_6), null); + + do_test_pending(); +} diff --git a/netwerk/test/unit/test_resumable_truncate.js b/netwerk/test/unit/test_resumable_truncate.js index 582f3691c8e5..46db02261351 100644 --- a/netwerk/test/unit/test_resumable_truncate.js +++ b/netwerk/test/unit/test_resumable_truncate.js @@ -8,8 +8,6 @@ function make_channel(url, callback, ctx) { return ios.newChannel(url, "", null); } -var do304 = false; - const responseBody = "response body"; function cachedHandler(metadata, response) { @@ -27,9 +25,6 @@ function cachedHandler(metadata, response) { // always respond to successful range requests with 206 response.setStatusLine(metadata.httpVersion, 206, "Partial Content"); response.setHeader("Content-Range", from + "-" + to + "/" + responseBody.length, false); - } else if (do304) { - response.setStatusLine(metadata.httpVersion, 304, "Not Modified"); - return; } response.setHeader("Content-Type", "text/plain", false); @@ -71,13 +66,11 @@ function finish_test() { } function start_cache_read() { - do304 = true; var chan = make_channel("http://localhost:4444/cached/test.gz"); chan.asyncOpen(new ChannelListener(finish_test, null), null); } function start_canceler() { - do304 = false; var chan = make_channel("http://localhost:4444/cached/test.gz"); chan.asyncOpen(new Canceler(start_cache_read), null); } @@ -87,7 +80,6 @@ function run_test() { httpserver.registerPathHandler("/cached/test.gz", cachedHandler); httpserver.start(4444); - do304 = false; var chan = make_channel("http://localhost:4444/cached/test.gz"); chan.asyncOpen(new ChannelListener(start_canceler, null), null); do_test_pending();