Bug 835872 - Handle download errors. r=enn

This commit is contained in:
Paolo Amadini 2013-02-02 00:59:21 +01:00
Родитель 7824a51ca0
Коммит 31f9e5ac5b
4 изменённых файлов: 175 добавлений и 3 удалений

Просмотреть файл

@ -19,6 +19,9 @@
* Represents the target of a download, for example a file in the global
* downloads directory, or a file in the system temporary directory.
*
* DownloadError
* Provides detailed information about a download failure.
*
* DownloadSaver
* Template for an object that actually transfers the data for the download.
*
@ -32,6 +35,7 @@ this.EXPORTED_SYMBOLS = [
"Download",
"DownloadSource",
"DownloadTarget",
"DownloadError",
"DownloadSaver",
"DownloadCopySaver",
];
@ -94,6 +98,13 @@ Download.prototype = {
*/
done: false,
/**
* When the download fails, this is set to a DownloadError instance indicating
* the cause of the failure. If the download has been completed successfully
* or has been canceled, this property is null.
*/
error: null,
/**
* Indicates whether this download's "progress" property is able to report
* partial progress while the download proceeds, and whether the value in
@ -166,6 +177,9 @@ Download.prototype = {
try {
yield this.saver.execute();
this.progress = 100;
} catch (ex) {
this.error = ex;
throw ex;
} finally {
this.done = true;
this._notifyChange();
@ -238,6 +252,64 @@ DownloadTarget.prototype = {
file: null,
};
////////////////////////////////////////////////////////////////////////////////
//// DownloadError
/**
* Provides detailed information about a download failure.
*
* @param aResult
* The result code associated with the error.
* @param aMessage
* The message to be displayed, or null to use the message associated
* with the result code.
* @param aInferCause
* If true, attempts to determine if the cause of the download is a
* network failure or a local file failure, based on a set of known
* values of the result code. This is useful when the error is received
* by a component that handles both aspects of the download.
*/
function DownloadError(aResult, aMessage, aInferCause)
{
const NS_ERROR_MODULE_BASE_OFFSET = 0x45;
const NS_ERROR_MODULE_NETWORK = 6;
const NS_ERROR_MODULE_FILES = 13;
// Set the error name used by the Error object prototype first.
this.name = "DownloadError";
this.result = aResult || Cr.NS_ERROR_FAILURE;
if (aMessage) {
this.message = aMessage;
} else {
let exception = new Components.Exception(this.result);
this.message = exception.toString();
}
if (aInferCause) {
let module = ((aResult & 0x7FFF0000) >> 16) - NS_ERROR_MODULE_BASE_OFFSET;
this.becauseSourceFailed = (module == NS_ERROR_MODULE_NETWORK);
this.becauseTargetFailed = (module == NS_ERROR_MODULE_FILES);
}
}
DownloadError.prototype = {
__proto__: Error.prototype,
/**
* The result code associated with this error.
*/
result: false,
/**
* Indicates an error occurred while reading from the remote location.
*/
becauseSourceFailed: false,
/**
* Indicates an error occurred while writing to the local target.
*/
becauseTargetFailed: false,
};
////////////////////////////////////////////////////////////////////////////////
//// DownloadSaver
@ -274,7 +346,7 @@ DownloadSaver.prototype = {
function DownloadCopySaver() { }
DownloadCopySaver.prototype = {
__proto__: DownloadSaver,
__proto__: DownloadSaver.prototype,
/**
* Implements "DownloadSaver.execute".
@ -296,8 +368,9 @@ DownloadCopySaver.prototype = {
if (Components.isSuccessCode(aStatus)) {
deferred.resolve();
} else {
deferred.reject(new Components.Exception("Download failed.",
aStatus));
// Infer the origin of the error from the failure code, because
// BackgroundFileSaver does not provide more specific data.
deferred.reject(new DownloadError(aStatus, null, true));
}
// Free the reference cycle, in order to release resources earlier.

Просмотреть файл

@ -124,4 +124,11 @@ this.Downloads = {
return aDownload.start();
});
},
/**
* Constructor for a DownloadError object. When you catch an exception during
* a download, you can use this to verify if "ex instanceof Downloads.Error",
* before reading the exception properties with the error details.
*/
Error: DownloadError,
};

Просмотреть файл

@ -34,10 +34,20 @@ XPCOMUtils.defineLazyModuleGetter(this, "Services",
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
const ServerSocket = Components.Constructor(
"@mozilla.org/network/server-socket;1",
"nsIServerSocket",
"init");
const HTTP_SERVER_PORT = 4444;
const HTTP_BASE = "http://localhost:" + HTTP_SERVER_PORT;
const FAKE_SERVER_PORT = 4445;
const FAKE_BASE = "http://localhost:" + FAKE_SERVER_PORT;
const TEST_SOURCE_URI = NetUtil.newURI(HTTP_BASE + "/source.txt");
const TEST_FAKE_SOURCE_URI = NetUtil.newURI(FAKE_BASE + "/source.txt");
const TEST_TARGET_FILE_NAME = "test-download.txt";
const TEST_DATA_SHORT = "This test string is downloaded.";
@ -107,6 +117,24 @@ function promiseVerifyContents(aFile, aExpectedContents)
return deferred.promise;
}
/**
* Starts a socket listener that closes each incoming connection.
*
* @returns nsIServerSocket that listens for connections. Call its "close"
* method to stop listening and free the server port.
*/
function startFakeServer()
{
let serverSocket = new ServerSocket(FAKE_SERVER_PORT, true, -1);
serverSocket.asyncListen({
onSocketAccepted: function (aServ, aTransport) {
aTransport.close(Cr.NS_BINDING_ABORTED);
},
onStopListening: function () { },
});
return serverSocket;
}
////////////////////////////////////////////////////////////////////////////////
//// Initialization functions common to all tests

Просмотреть файл

@ -133,3 +133,67 @@ add_task(function test_download_intermediate_progress()
yield promiseVerifyContents(targetFile, TEST_DATA_SHORT + TEST_DATA_SHORT);
});
/**
* Ensures download error details are reported on network failures.
*/
add_task(function test_download_error_source()
{
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
let serverSocket = startFakeServer();
try {
let download = yield Downloads.createDownload({
source: { uri: TEST_FAKE_SOURCE_URI },
target: { file: targetFile },
saver: { type: "copy" },
});
do_check_true(download.error === null);
try {
yield download.start();
do_throw("The download should have failed.");
} catch (ex if ex instanceof Downloads.Error && ex.becauseSourceFailed) {
// A specific error object is thrown when reading from the source fails.
}
do_check_true(download.done);
do_check_true(download.error !== null);
do_check_true(download.error.becauseSourceFailed);
do_check_false(download.error.becauseTargetFailed);
} finally {
serverSocket.close();
}
});
/**
* Ensures download error details are reported on local writing failures.
*/
add_task(function test_download_error_target()
{
let targetFile = getTempFile(TEST_TARGET_FILE_NAME);
// Create a file without write access permissions.
targetFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0);
let download = yield Downloads.createDownload({
source: { uri: TEST_SOURCE_URI },
target: { file: targetFile },
saver: { type: "copy" },
});
do_check_true(download.error === null);
try {
yield download.start();
do_throw("The download should have failed.");
} catch (ex if ex instanceof Downloads.Error && ex.becauseTargetFailed) {
// A specific error object is thrown when writing to the target fails.
}
do_check_true(download.done);
do_check_true(download.error !== null);
do_check_true(download.error.becauseTargetFailed);
do_check_false(download.error.becauseSourceFailed);
});