Add a timeout on SDK calls to allow specific error messages.

## Issue 1263
Previously, we tried to inform the user that when an SDK call such as `support_create` and `publish` fails (specifically, timed out), the operation could be successful -- please check the transactions later.

However, we only covered the case of `fetch` actually getting a response that indicated a timeout, e.g. "status = 524". For our SDK case, the timeout scenario is an error that goes into the `catch` block. In the `catch` block, we can't differentiate whether it is a timeout because it only returns a generic "failed to fetch" message.

## New Approach
Since `fetch` does not support a timeout value, the usual solution is to wrap it with a `setTimeout`. This already exists in our code as `fetchWithTimeout` (yay).

By setting a timeout that is lower than the browser's default and also lower than the SDK operation (90s for most commands, 5m for `publish`), we would now have a way to detect a timeout and inform the user.

Firefox's 90s seems to be the lowest common denominator ... so 60s was chosen as the default (added some buffer).

For the case of 'publish', it is actually called in the backend, so wrap the xhr call with a timeout as well.
This commit is contained in:
infinite-persistence 2022-04-30 13:27:43 +08:00 committed by Thomas Zarebczan
parent cd2998d695
commit 9b44b7eb91
4 changed files with 42 additions and 10 deletions

View file

@ -1,3 +1,4 @@
export const ALREADY_CLAIMED = 'once the invite reward has been claimed the referrer cannot be changed';
export const REFERRER_NOT_FOUND = 'A odysee account could not be found for the referrer you provided.';
export const PUBLISH_TIMEOUT_BUT_LIKELY_SUCCESSFUL = 'There was a network error, but the publish may have been completed. Wait a few minutes, then check your Uploads or Wallet page to confirm.';
export const FETCH_TIMEOUT = 'promise timeout';

View file

@ -1,5 +1,7 @@
// @flow
import { FETCH_TIMEOUT } from 'constants/errors';
import { NO_AUTH, X_LBRY_AUTH_TOKEN } from 'constants/token';
import fetchWithTimeout from 'util/fetch';
require('proxy-polyfill');
@ -222,21 +224,35 @@ const ApiFailureMgr = {
},
};
function checkAndParse(response: Response, method: string) {
if (!response.ok) {
/**
* Returns a customized error message for known scenarios.
*/
function resolveFetchErrorMsg(method: string, response: Response | string) {
if (typeof response === 'object') {
// prettier-ignore
switch (response.status) {
case 504: // Gateway timeout
case 524: // Cloudflare: a timeout occurred
switch (method) {
case 'publish':
throw Error(__('[Publish]: Your action timed out, but may have been completed. Refresh and check your Uploads or Wallet page to confirm after a few minutes.'));
return __('[Publish]: Your action timed out, but may have been completed. Refresh and check your Uploads or Wallet page to confirm after a few minutes.');
default:
throw Error(`${method}: ${response.statusText} (${response.status})`);
return `${method}: ${response.statusText} (${response.status})`;
}
default:
throw Error(`${method}: ${response.statusText} (${response.status})`);
return `${method}: ${response.statusText} (${response.status})`;
}
} else if (response === FETCH_TIMEOUT) {
return `${method}: Your action timed out, but may have been completed.`;
} else {
return `${method}: fetch failed.`;
}
}
function checkAndParse(response: Response, method: string) {
if (!response.ok) {
const errMsg = resolveFetchErrorMsg(method, response);
throw Error(errMsg);
}
if (response.status >= 200 && response.status < 300) {
@ -289,7 +305,8 @@ export function apiCall(method: string, params: ?{}, resolve: Function, reject:
? Lbry.alternateConnectionString
: Lbry.daemonConnectionString;
return fetch(connectionString + '?m=' + method, options)
const SDK_FETCH_TIMEOUT_MS = 60000;
return fetchWithTimeout(SDK_FETCH_TIMEOUT_MS, fetch(connectionString + '?m=' + method, options))
.then((response) => checkAndParse(response, method))
.then((response) => {
const error = response.error || (response.result && response.result.error);
@ -303,7 +320,11 @@ export function apiCall(method: string, params: ?{}, resolve: Function, reject:
})
.catch((err) => {
ApiFailureMgr.logFailure(method, params, counter);
return reject(err);
if (err?.message === FETCH_TIMEOUT) {
reject(resolveFetchErrorMsg(method, FETCH_TIMEOUT));
} else {
reject(err);
}
});
}

View file

@ -1,14 +1,16 @@
import { FETCH_TIMEOUT } from 'constants/errors';
export default function fetchWithTimeout(ms, promise) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('promise timeout'));
reject(new Error(FETCH_TIMEOUT));
}, ms);
promise.then(
res => {
(res) => {
clearTimeout(timeoutId);
resolve(res);
},
err => {
(err) => {
clearTimeout(timeoutId);
reject(err);
}

View file

@ -5,6 +5,7 @@
// - 'file' binary
// - 'json_payload' publish params to be passed to the server's sdk.
import { PUBLISH_TIMEOUT_BUT_LIKELY_SUCCESSFUL } from '../../ui/constants/errors';
import { X_LBRY_AUTH_TOKEN } from '../../ui/constants/token';
import { doUpdateUploadAdd, doUpdateUploadProgress, doUpdateUploadRemove } from '../../ui/redux/actions/publish';
import { LBRY_WEB_PUBLISH_API } from 'config';
@ -12,6 +13,8 @@ import { LBRY_WEB_PUBLISH_API } from 'config';
const ENDPOINT = LBRY_WEB_PUBLISH_API;
const ENDPOINT_METHOD = 'publish';
const PUBLISH_FETCH_TIMEOUT_MS = 60000;
export function makeUploadRequest(
token: string,
params: FileUploadSdkParams,
@ -46,6 +49,7 @@ export function makeUploadRequest(
let xhr = new XMLHttpRequest();
xhr.open('POST', ENDPOINT);
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
xhr.timeout = PUBLISH_FETCH_TIMEOUT_MS;
xhr.responseType = 'json';
xhr.upload.onprogress = (e) => {
const percentage = ((e.loaded / e.total) * 100).toFixed(2);
@ -59,6 +63,10 @@ export function makeUploadRequest(
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'error' }));
reject(new Error(__('There was a problem with your upload. Please try again.')));
};
xhr.ontimeout = () => {
window.store.dispatch(doUpdateUploadProgress({ guid, status: 'error' }));
reject(new Error(PUBLISH_TIMEOUT_BUT_LIKELY_SUCCESSFUL));
};
xhr.onabort = () => {
window.store.dispatch(doUpdateUploadRemove(guid));
};