diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index 251ce19ec3..102a3b7833 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -12,6 +12,7 @@ Zotero.HTTP = new function() { this.UnexpectedStatusException = function(xmlhttp, msg) { this.xmlhttp = xmlhttp; this.status = xmlhttp.status; + this.channel = xmlhttp.channel; this.message = msg; // Hide password from debug output diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js index 00adb3e45f..ac305237ed 100644 --- a/chrome/content/zotero/xpcom/sync.js +++ b/chrome/content/zotero/xpcom/sync.js @@ -754,40 +754,7 @@ Zotero.Sync.Server = new function () { function _checkResponse(xmlhttp, noReloadOnFailure) { - if (!xmlhttp.responseText) { - var channel = xmlhttp.channel; - // Check SSL cert - if (channel) { - var secInfo = channel.securityInfo; - if (secInfo instanceof Ci.nsITransportSecurityInfo) { - secInfo.QueryInterface(Ci.nsITransportSecurityInfo); - if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) == Ci.nsIWebProgressListener.STATE_IS_INSECURE) { - var url = channel.name; - var ios = Components.classes["@mozilla.org/network/io-service;1"]. - getService(Components.interfaces.nsIIOService); - try { - var uri = ios.newURI(url, null, null); - var host = uri.host; - } - catch (e) { - Zotero.debug(e); - } - var kbURL = 'https://zotero.org/support/kb/ssl_certificate_error'; - _error(Zotero.getString('sync.storage.error.webdav.sslCertificateError', host) + "\n\n" - + Zotero.getString('general.seeForMoreInformation', kbURL), - false, noReloadOnFailure); - } - else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) == Ci.nsIWebProgressListener.STATE_IS_BROKEN) { - _error(Zotero.getString('sync.error.sslConnectionError'), false, noReloadOnFailure); - } - } - } - if (xmlhttp.status === 0) { - _error(Zotero.getString('sync.error.checkConnection'), false, noReloadOnFailure); - } - _error(Zotero.getString('sync.error.emptyResponseServer') + Zotero.getString('general.tryAgainLater'), - false, noReloadOnFailure); - } + if (!xmlhttp.responseXML || !xmlhttp.responseXML.childNodes[0] || xmlhttp.responseXML.childNodes[0].tagName != 'response' || diff --git a/chrome/content/zotero/xpcom/sync/syncAPIClient.js b/chrome/content/zotero/xpcom/sync/syncAPIClient.js index 3efd3274d0..9099b1eda1 100644 --- a/chrome/content/zotero/xpcom/sync/syncAPIClient.js +++ b/chrome/content/zotero/xpcom/sync/syncAPIClient.js @@ -524,6 +524,7 @@ Zotero.Sync.APIClient.prototype = { catch (e) { tries++; if (e instanceof Zotero.HTTP.UnexpectedStatusException) { + this._checkConnection(e.xmlhttp, e.channel); //this._checkRetry(e.xmlhttp); if (e.is5xx()) { @@ -566,6 +567,70 @@ Zotero.Sync.APIClient.prototype = { }, + /** + * Check connection for certificate errors, interruptions, and empty responses and + * throw an appropriate error + */ + _checkConnection: function (xmlhttp, channel) { + const Ci = Components.interfaces; + + if (!xmlhttp.responseText) { + let msg = null; + let dialogButtonText = null; + let dialogButtonCallback = null; + + // Check SSL cert + if (channel) { + let secInfo = channel.securityInfo; + if (secInfo instanceof Ci.nsITransportSecurityInfo) { + secInfo.QueryInterface(Ci.nsITransportSecurityInfo); + if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) + == Ci.nsIWebProgressListener.STATE_IS_INSECURE) { + let url = channel.name; + let ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + try { + var uri = ios.newURI(url, null, null); + var host = uri.host; + } + catch (e) { + Zotero.debug(e); + } + let kbURL = 'https://www.zotero.org/support/kb/ssl_certificate_error'; + msg = Zotero.getString('sync.storage.error.webdav.sslCertificateError', host); + dialogButtonText = Zotero.getString('general.moreInformation'); + dialogButtonCallback = function () { + let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + let win = wm.getMostRecentWindow("navigator:browser"); + win.ZoteroPane.loadURI(kbURL, { metaKey: true, shiftKey: true }); + }; + } + else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) + == Ci.nsIWebProgressListener.STATE_IS_BROKEN) { + msg = Zotero.getString('sync.error.sslConnectionError'); + } + } + } + if (!msg && xmlhttp.status === 0) { + msg = Zotero.getString('sync.error.checkConnection'); + } + if (!msg) { + msg = Zotero.getString('sync.error.emptyResponseServer') + + Zotero.getString('general.tryAgainLater'); + } + throw new Zotero.Error( + msg, + 0, + { + dialogButtonText, + dialogButtonCallback + } + ); + } + }, + + _checkBackoff: function (xmlhttp) { var backoff = xmlhttp.getResponseHeader("Backoff"); if (backoff) { diff --git a/test/content/support.js b/test/content/support.js index 6e454be9c9..8f85f9f071 100644 --- a/test/content/support.js +++ b/test/content/support.js @@ -710,7 +710,7 @@ function setHTTPResponse(server, baseURL, response, responses) { response = responses[topic][key]; } - var responseArray = [response.status || 200, {}, ""]; + var responseArray = [response.status !== undefined ? response.status : 200, {}, ""]; if (response.json) { responseArray[1]["Content-Type"] = "application/json"; responseArray[2] = JSON.stringify(response.json); diff --git a/test/tests/syncAPIClientTest.js b/test/tests/syncAPIClientTest.js new file mode 100644 index 0000000000..ef0b5c808b --- /dev/null +++ b/test/tests/syncAPIClientTest.js @@ -0,0 +1,71 @@ +"use strict"; + +describe("Zotero.Sync.APIClient", function () { + Components.utils.import("resource://zotero/config.js"); + + var apiKey = Zotero.Utilities.randomString(24); + var baseURL = "http://local.zotero/"; + var server, client; + + function setResponse(response) { + setHTTPResponse(server, baseURL, response, {}); + } + + before(function () { + Components.utils.import("resource://zotero/concurrentCaller.js"); + var caller = new ConcurrentCaller(1); + caller.setLogger(msg => Zotero.debug(msg)); + caller.stopOnError = true; + caller.onError = function (e) { + Zotero.logError(e); + if (e.fatal) { + caller.stop(); + throw e; + } + }; + + Zotero.HTTP.mock = sinon.FakeXMLHttpRequest; + + client = new Zotero.Sync.APIClient({ + baseURL, + apiVersion: ZOTERO_CONFIG.API_VERSION, + apiKey, + caller + }) + }) + + beforeEach(function () { + server = sinon.fakeServer.create(); + server.autoRespond = true; + }) + + after(function () { + Zotero.HTTP.mock = null; + }) + + describe("#_checkConnection()", function () { + it("should catch an error with an empty response", function* () { + setResponse({ + method: "GET", + url: "error", + status: 500, + text: "" + }) + var e = yield getPromiseError(client.makeRequest("GET", baseURL + "error")); + assert.ok(e); + assert.isTrue(e.message.startsWith(Zotero.getString('sync.error.emptyResponseServer'))); + }) + + it("should catch an interrupted connection", function* () { + setResponse({ + method: "GET", + url: "empty", + status: 0, + text: "" + }) + var e = yield getPromiseError(client.makeRequest("GET", baseURL + "empty")); + assert.ok(e); + assert.equal(e.message, Zotero.getString('sync.error.checkConnection')); + }) + }) +})