diff --git a/ReactNativeClient/lib/WebDavApi.js b/ReactNativeClient/lib/WebDavApi.js index daa5a7b9a1..84a1ada1d9 100644 --- a/ReactNativeClient/lib/WebDavApi.js +++ b/ReactNativeClient/lib/WebDavApi.js @@ -16,6 +16,38 @@ class WebDavApi { constructor(options) { this.logger_ = new Logger(); this.options_ = options; + this.lastRequests_ = []; + } + + logRequest_(request, responseText) { + if (this.lastRequests_.length > 10) this.lastRequests_.splice(0, 1); + + const serializeRequest = (r) => { + const options = Object.assign({}, r.options); + if (typeof options.body === 'string') options.body = options.body.substr(0, 4096); + const output = []; + output.push(options.method ? options.method : 'GET'); + output.push(r.url); + options.headers = Object.assign({}, options.headers); + if (options.headers['Authorization']) options.headers['Authorization'] = '********'; + delete options.method; + output.push(JSON.stringify(options)); + return output.join(' '); + }; + + this.lastRequests_.push({ + timestamp: Date.now(), + request: serializeRequest(request), + response: responseText ? responseText.substr(0, 4096) : '', + }); + } + + lastRequests() { + return this.lastRequests_; + } + + clearLastRequests() { + this.lastRequests_ = []; } setLogger(l) { @@ -340,6 +372,8 @@ class WebDavApi { const responseText = await response.text(); + this.logRequest_({ url: url, options: fetchOptions }, responseText); + // console.info('WebDAV Response', responseText); // Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier diff --git a/ReactNativeClient/lib/file-api-driver-webdav.js b/ReactNativeClient/lib/file-api-driver-webdav.js index fe9614fcb7..4962e1ff81 100644 --- a/ReactNativeClient/lib/file-api-driver-webdav.js +++ b/ReactNativeClient/lib/file-api-driver-webdav.js @@ -15,6 +15,14 @@ class FileApiDriverWebDav { return 3; } + lastRequests() { + return this.api().lastRequests(); + } + + clearLastRequests() { + return this.api().clearLastRequests(); + } + async stat(path) { try { const result = await this.api().execPropFind(path, 0, ['d:getlastmodified', 'd:resourcetype']); diff --git a/ReactNativeClient/lib/file-api.js b/ReactNativeClient/lib/file-api.js index 113af7cec1..52d9ac51ef 100644 --- a/ReactNativeClient/lib/file-api.js +++ b/ReactNativeClient/lib/file-api.js @@ -9,8 +9,15 @@ const { time } = require('lib/time-utils.js'); function requestCanBeRepeated(error) { const errorCode = typeof error === 'object' && error.code ? error.code : null; + // The target is explicitely rejecting the item so repeating wouldn't make a difference. if (errorCode === 'rejectedByTarget') return false; + // We don't repeat failSafe errors because it's an indication of an issue at the + // server-level issue which usually cannot be fixed by repeating the request. + // Also we print the previous requests and responses to the log in this case, + // so not repeating means there will be less noise in the log. + if (errorCode === 'failSafe') return false; + return true; } @@ -60,6 +67,14 @@ class FileApi { return 0; } + lastRequests() { + return this.driver_.lastRequests ? this.driver_.lastRequests() : []; + } + + clearLastRequests() { + if (this.driver_.clearLastRequests) this.driver_.clearLastRequests(); + } + baseDir() { return this.baseDir_; } diff --git a/ReactNativeClient/lib/synchronizer.js b/ReactNativeClient/lib/synchronizer.js index c309aecb43..1b4bd9c6eb 100644 --- a/ReactNativeClient/lib/synchronizer.js +++ b/ReactNativeClient/lib/synchronizer.js @@ -173,6 +173,17 @@ class Synchronizer { return this.cancelling_; } + logLastRequests() { + const lastRequests = this.api().lastRequests(); + if (!lastRequests || !lastRequests.length) return; + + for (const r of lastRequests) { + const timestamp = time.unixMsToLocalHms(r.timestamp); + this.logger().info(`Req ${timestamp}: ${r.request}`); + this.logger().info(`Res ${timestamp}: ${r.response}`); + } + } + static stateToLabel(state) { if (state === 'idle') return _('Idle'); if (state === 'in_progress') return _('In progress'); @@ -703,6 +714,8 @@ class Synchronizer { // in the application, and needs to be resolved by the user. // Or it's a temporary issue that will be resolved on next sync. this.logger().info(error.message); + + if (error.code === 'failSafe') this.logLastRequests(); } else if (error.code === 'unknownItemType') { this.progressReport_.errors.push(_('Unknown item type downloaded - please upgrade Joplin to the latest version')); this.logger().error(error); @@ -710,7 +723,10 @@ class Synchronizer { this.logger().error(error); // Don't save to the report errors that are due to things like temporary network errors or timeout. - if (!shim.fetchRequestCanBeRetried(error)) this.progressReport_.errors.push(error); + if (!shim.fetchRequestCanBeRetried(error)) { + this.progressReport_.errors.push(error); + this.logLastRequests(); + } } } diff --git a/ReactNativeClient/lib/time-utils.js b/ReactNativeClient/lib/time-utils.js index f052650a8b..0b4232477c 100644 --- a/ReactNativeClient/lib/time-utils.js +++ b/ReactNativeClient/lib/time-utils.js @@ -74,6 +74,10 @@ class Time { return moment.unix(ms / 1000).format('DD/MM/YYYY HH:mm'); } + unixMsToLocalHms(ms) { + return moment.unix(ms / 1000).format('HH:mm:ss'); + } + formatMsToLocal(ms, format = null) { if (format === null) format = this.dateTimeFormat(); return moment(ms).format(format);