diff options
author | Christoph Wurst <ChristophWurst@users.noreply.github.com> | 2020-02-04 18:18:17 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-04 18:18:17 +0300 |
commit | b641959fa8a792c70df69dd9f98eab39ad9cdb70 (patch) | |
tree | 12af87fabafb22f1b9ea8dca2b2ac7a6f8a07c60 /src | |
parent | cd2375b5fd8d1defa8c5a2e55d8332aea5e2d556 (diff) | |
parent | cda1eb7b4fdf0e8c5bdb1884f2605dc45b98bc71 (diff) |
Merge pull request #2612 from nextcloud/enhancement/envelope-fetch-conflict-retry
Try to recover from server errors
Diffstat (limited to 'src')
-rw-r--r-- | src/components/FolderContent.vue | 89 | ||||
-rw-r--r-- | src/errors/MailboxLockedError.js | 28 | ||||
-rw-r--r-- | src/errors/MailboxNotCachedError.js | 28 | ||||
-rw-r--r-- | src/errors/convert.js | 51 | ||||
-rw-r--r-- | src/errors/match.js | 34 | ||||
-rw-r--r-- | src/service/MessageService.js | 21 | ||||
-rw-r--r-- | src/tests/unit/errors/convert.spec.js | 69 | ||||
-rw-r--r-- | src/tests/unit/errors/match.spec.js | 56 | ||||
-rw-r--r-- | src/tests/unit/util/wait.spec.js | 28 | ||||
-rw-r--r-- | src/util/wait.js | 26 |
10 files changed, 375 insertions, 55 deletions
diff --git a/src/components/FolderContent.vue b/src/components/FolderContent.vue index ead4bce5f..666682313 100644 --- a/src/components/FolderContent.vue +++ b/src/components/FolderContent.vue @@ -34,10 +34,13 @@ import EnvelopeList from './EnvelopeList' import Error from './Error' import Loading from './Loading' import logger from '../logger' -import {MailboxNotCachedException} from '../service/MessageService' +import MailboxNotCachedError from '../errors/MailboxNotCachedError' import Message from './Message' import NewMessageDetail from './NewMessageDetail' import NoMessageSelected from './NoMessageSelected' +import {matchError} from '../errors/match' +import MailboxLockedError from '../errors/MailboxLockedError' +import {wait} from '../util/wait' export default { name: 'FolderContent', @@ -132,52 +135,64 @@ export default { return this.loadEnvelopes() }) }, - loadEnvelopes() { + async loadEnvelopes() { this.loadingEnvelopes = true this.error = false - this.$store - .dispatch('fetchEnvelopes', { + try { + await this.$store.dispatch('fetchEnvelopes', { accountId: this.account.id, folderId: this.folder.id, query: this.searchQuery, }) - .then(() => { - const envelopes = this.envelopes - logger.debug('envelopes fetched', envelopes) - - this.loadingEnvelopes = false - - if (!this.isMobile && this.$route.name !== 'message' && envelopes.length > 0) { - // Show first message - let first = envelopes[0] - - // Keep the selected account-folder combination, but navigate to the message - // (it's not a bug that we don't use first.accountId and first.folderId here) - this.$router.replace({ - name: 'message', - params: { - accountId: this.account.id, - folderId: this.folder.id, - messageUid: first.uid, - }, - }) - } - }) - .catch(error => { - if (error instanceof MailboxNotCachedException) { + + const envelopes = this.envelopes + logger.debug('envelopes fetched', envelopes) + + this.loadingEnvelopes = false + + if (!this.isMobile && this.$route.name !== 'message' && envelopes.length > 0) { + // Show first message + let first = envelopes[0] + + // Keep the selected account-folder combination, but navigate to the message + // (it's not a bug that we don't use first.accountId and first.folderId here) + this.$router.replace({ + name: 'message', + params: { + accountId: this.account.id, + folderId: this.folder.id, + messageUid: first.uid, + }, + }) + } + } catch (error) { + await matchError(error, { + [MailboxLockedError.name]: async error => { + logger.info('Mailbox is locked', {error}) + + await wait(15 * 1000) + // Keep trying + await this.loadEnvelopes() + }, + [MailboxNotCachedError.name]: async error => { + logger.info('Mailbox not cached. Triggering initialization', {error}) this.loadingEnvelopes = false - logger.info('Mailbox not cached. Triggering initialization') - return this.initializeCache() - } - logger.error('Could not fetch envelopes', {error}) - this.error = error - }) - .catch(error => { - logger.error('Could not fetch envelopes or initialize cache') - this.error = error + try { + await this.initializeCache() + } catch (error) { + logger.error('Could not initialize cache', {error}) + this.error = error + } + }, + default: error => { + logger.error('Could not fetch envelopes', {error}) + this.loadingEnvelopes = false + this.error = error + }, }) + } }, hideMessage() { this.$router.replace({ diff --git a/src/errors/MailboxLockedError.js b/src/errors/MailboxLockedError.js new file mode 100644 index 000000000..e9eb55351 --- /dev/null +++ b/src/errors/MailboxLockedError.js @@ -0,0 +1,28 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +export default class MailboxLockedError extends Error { + constructor(message) { + super(message) + this.name = 'MailboxLockedError' + this.message = message + } +} diff --git a/src/errors/MailboxNotCachedError.js b/src/errors/MailboxNotCachedError.js new file mode 100644 index 000000000..12fc4a391 --- /dev/null +++ b/src/errors/MailboxNotCachedError.js @@ -0,0 +1,28 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +export default class MailboxNotCachedError extends Error { + constructor(message) { + super(message) + this.name = 'MailboxNotCachedError' + this.message = message + } +} diff --git a/src/errors/convert.js b/src/errors/convert.js new file mode 100644 index 000000000..4d816fe15 --- /dev/null +++ b/src/errors/convert.js @@ -0,0 +1,51 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import MailboxLockedError from './MailboxLockedError' +import MailboxNotCachedError from './MailboxNotCachedError' + +const map = { + 'OCA\\Mail\\Exception\\MailboxLockedException': MailboxLockedError, + 'OCA\\Mail\\Exception\\MailboxNotCachedException': MailboxNotCachedError, +} + +/** + * @param {object} axiosError + */ +export const convertAxiosError = axiosError => { + if (!('response' in axiosError)) { + // No conversion + return axiosError + } + + if (!('x-mail-response' in axiosError.response.headers)) { + // Not a structured response + return axiosError + } + + const response = axiosError.response + if (!(response.data.data.type in map)) { + // No conversion possible + return axiosError + } + + return new map[response.data.data.type]() +} diff --git a/src/errors/match.js b/src/errors/match.js new file mode 100644 index 000000000..6167a3d8e --- /dev/null +++ b/src/errors/match.js @@ -0,0 +1,34 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +/** + * @param {Error} error + * @param {object} matches + */ +export const matchError = async (error, matches) => { + if (error.name in matches) { + return await Promise.resolve(matches[error.name](error)) + } + if ('default' in matches) { + return await Promise.resolve(matches['default'](error)) + } + throw new Error('unhandled error in match: ' + error.name) +} diff --git a/src/service/MessageService.js b/src/service/MessageService.js index 9e2fdbfac..e61bff436 100644 --- a/src/service/MessageService.js +++ b/src/service/MessageService.js @@ -2,15 +2,9 @@ import {generateUrl} from '@nextcloud/router' import axios from '@nextcloud/axios' import logger from '../logger' +import MailboxNotCachedError from '../errors/MailboxNotCachedError' import {parseErrorResponse} from '../http/ErrorResponseParser' - -export class MailboxNotCachedException extends Error { - constructor(message) { - super(message) - this.name = 'MailboxNotCachedException' - this.message = message - } -} +import {convertAxiosError} from '../errors/convert' export function fetchEnvelopes(accountId, folderId, query, cursor) { const url = generateUrl('/apps/mail/api/accounts/{accountId}/folders/{folderId}/messages', { @@ -32,16 +26,7 @@ export function fetchEnvelopes(accountId, folderId, query, cursor) { }) .then(resp => resp.data) .catch(error => { - if ( - error.response && - error.response.status === 400 && - 'x-mail-response' in error.response.headers && - error.response.data.data.type === 'OCA\\Mail\\Exception\\MailboxNotCachedException' - ) { - logger.debug(`mailbox ${folderId} of account ${accountId} is not cached`) - throw new MailboxNotCachedException() - } - throw error + throw convertAxiosError(error) }) } diff --git a/src/tests/unit/errors/convert.spec.js b/src/tests/unit/errors/convert.spec.js new file mode 100644 index 000000000..a722e3baf --- /dev/null +++ b/src/tests/unit/errors/convert.spec.js @@ -0,0 +1,69 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import {convertAxiosError} from '../../../errors/convert' + +describe('convert error', () => { + it('ignores errors without a response', () => { + const error = {} // no response + + const result = convertAxiosError(error) + + expect(result).to.not.be.an.instanceof(Error) + expect(result).to.equal(error) + }) + + it('ignores errors it does not know', () => { + const error = { + response: { + headers: {}, + status: 400, + data: {}, + }, + } + + const result = convertAxiosError(error) + + expect(result).to.not.be.an.instanceof(Error) + expect(result).to.equal(error) + }) + + it('converts known exceptions to errors', () => { + const error = { + response: { + headers: { + 'x-mail-response': '1', + }, + status: 400, + data: { + status: 'fail', + data: { + type: 'OCA\\Mail\\Exception\\MailboxLockedException', + }, + }, + }, + } + + const result = convertAxiosError(error) + + expect(result).to.be.an.instanceof(Error) + }) +}) diff --git a/src/tests/unit/errors/match.spec.js b/src/tests/unit/errors/match.spec.js new file mode 100644 index 000000000..bf0897161 --- /dev/null +++ b/src/tests/unit/errors/match.spec.js @@ -0,0 +1,56 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import {matchError} from '../../../errors/match' + +describe('match', () => { + it('throws an error when nothing matches', done => { + const error = new Error('henlo') + + matchError(error, {}).catch(() => done()) + }) + + it('uses the default', done => { + const map = { + default: error => 3, + } + const error = new Error('henlo') + + matchError(error, map).then(result => { + expect(expect(result).to.equal(3)) + done() + }) + }) + + it('matches errors', done => { + const map = { + MyErr: error => 2, + default: error => 3, + } + const error = new Error('henlo') + error.name = 'MyErr' + + matchError(error, map).then(result => { + expect(expect(result).to.equal(2)) + done() + }) + }) +}) diff --git a/src/tests/unit/util/wait.spec.js b/src/tests/unit/util/wait.spec.js new file mode 100644 index 000000000..90a74ae3c --- /dev/null +++ b/src/tests/unit/util/wait.spec.js @@ -0,0 +1,28 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import {wait} from '../../../util/wait' + +describe('wait', () => { + it('waits', done => { + wait(0).then(done) + }) +}) diff --git a/src/util/wait.js b/src/util/wait.js new file mode 100644 index 000000000..7a9dff8be --- /dev/null +++ b/src/util/wait.js @@ -0,0 +1,26 @@ +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +export const wait = ms => { + return new Promise(res => { + setTimeout(res, ms) + }) +} |