chore: add timeout mechanism for fetch calls (#3649)

This commit is contained in:
Kunal Kundu
2020-05-12 19:21:13 +05:30
committed by GitHub
parent 334304ed52
commit 3e34e52440
11 changed files with 66 additions and 28 deletions

View File

@ -1,5 +1,5 @@
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import { ApiRequest, PointerFile } from 'netlify-cms-lib-util'; import { ApiRequest, PointerFile, unsentRequest } from 'netlify-cms-lib-util';
type MakeAuthorizedRequest = (req: ApiRequest) => Promise<Response>; type MakeAuthorizedRequest = (req: ApiRequest) => Promise<Response>;
@ -63,7 +63,7 @@ export class GitLfsClient {
} }
private async doUpload(upload: LfsBatchAction, resource: Blob) { private async doUpload(upload: LfsBatchAction, resource: Blob) {
await fetch(decodeURI(upload.href), { await unsentRequest.fetchWithTimeout(decodeURI(upload.href), {
method: 'PUT', method: 'PUT',
body: resource, body: resource,
headers: upload.header, headers: upload.header,

View File

@ -32,6 +32,7 @@ describe('github API', () => {
Authorization: 'Bearer token', Authorization: 'Bearer token',
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
signal: expect.any(AbortSignal),
}); });
}); });

View File

@ -211,30 +211,31 @@ export default class GitGateway implements Implementation {
gitlab_enabled: gitlabEnabled, gitlab_enabled: gitlabEnabled,
bitbucket_enabled: bitbucketEnabled, bitbucket_enabled: bitbucketEnabled,
roles, roles,
} = await fetch(`${this.gatewayUrl}/settings`, { } = await unsentRequest
headers: { Authorization: `Bearer ${token}` }, .fetchWithTimeout(`${this.gatewayUrl}/settings`, {
}).then(async res => { headers: { Authorization: `Bearer ${token}` },
const contentType = res.headers.get('Content-Type') || ''; })
if (!contentType.includes('application/json') && !contentType.includes('text/json')) { .then(async res => {
throw new APIError( const contentType = res.headers.get('Content-Type') || '';
`Your Git Gateway backend is not returning valid settings. Please make sure it is enabled.`, if (!contentType.includes('application/json') && !contentType.includes('text/json')) {
res.status, throw new APIError(
'Git Gateway', `Your Git Gateway backend is not returning valid settings. Please make sure it is enabled.`,
); res.status,
} 'Git Gateway',
);
}
const body = await res.json();
const body = await res.json(); if (!res.ok) {
throw new APIError(
`Git Gateway Error: ${body.message ? body.message : body}`,
res.status,
'Git Gateway',
);
}
if (!res.ok) { return body;
throw new APIError( });
`Git Gateway Error: ${body.message ? body.message : body}`,
res.status,
'Git Gateway',
);
}
return body;
});
this.acceptRoles = roles; this.acceptRoles = roles;
if (githubEnabled) { if (githubEnabled) {
this.backendType = 'github'; this.backendType = 'github';

View File

@ -1,6 +1,6 @@
import { flow, fromPairs, map } from 'lodash/fp'; import { flow, fromPairs, map } from 'lodash/fp';
import minimatch from 'minimatch'; import minimatch from 'minimatch';
import { ApiRequest, PointerFile } from 'netlify-cms-lib-util'; import { ApiRequest, PointerFile, unsentRequest } from 'netlify-cms-lib-util';
type MakeAuthorizedRequest = (req: ApiRequest) => Promise<Response>; type MakeAuthorizedRequest = (req: ApiRequest) => Promise<Response>;
@ -96,7 +96,7 @@ const getResourceUploadURLs = async (
}; };
const uploadBlob = (uploadURL: string, blob: Blob) => const uploadBlob = (uploadURL: string, blob: Blob) =>
fetch(uploadURL, { unsentRequest.fetchWithTimeout(uploadURL, {
method: 'PUT', method: 'PUT',
body: blob, body: blob,
}); });

View File

@ -117,6 +117,7 @@ describe('github API', () => {
Authorization: 'token token', Authorization: 'token token',
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
signal: expect.any(AbortSignal),
}); });
}); });
@ -163,6 +164,7 @@ describe('github API', () => {
Authorization: 'promise-token', Authorization: 'promise-token',
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
}, },
signal: expect.any(AbortSignal),
}); });
}); });
}); });

View File

@ -43,6 +43,7 @@ describe('github backend implementation', () => {
headers: { headers: {
Authorization: 'token token', Authorization: 'token token',
}, },
signal: expect.any(AbortSignal),
}); });
}); });

View File

@ -28,6 +28,7 @@ import {
runWithLock, runWithLock,
blobToFileObj, blobToFileObj,
contentKeyFromBranch, contentKeyFromBranch,
unsentRequest,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import AuthenticationPage from './AuthenticationPage'; import AuthenticationPage from './AuthenticationPage';
import { Octokit } from '@octokit/rest'; import { Octokit } from '@octokit/rest';
@ -40,6 +41,8 @@ const MAX_CONCURRENT_DOWNLOADS = 10;
type ApiFile = { id: string; type: string; name: string; path: string; size: number }; type ApiFile = { id: string; type: string; name: string; path: string; size: number };
const { fetchWithTimeout: fetch } = unsentRequest;
export default class GitHub implements Implementation { export default class GitHub implements Implementation {
lock: AsyncLock; lock: AsyncLock;
api: API | null; api: API | null;

View File

@ -8,6 +8,7 @@ import {
ImplementationFile, ImplementationFile,
EditorialWorkflowError, EditorialWorkflowError,
APIError, APIError,
unsentRequest,
} from 'netlify-cms-lib-util'; } from 'netlify-cms-lib-util';
import AuthenticationPage from './AuthenticationPage'; import AuthenticationPage from './AuthenticationPage';
@ -83,13 +84,14 @@ export default class ProxyBackend implements Implementation {
} }
async request(payload: { action: string; params: Record<string, unknown> }) { async request(payload: { action: string; params: Record<string, unknown> }) {
const response = await fetch(this.proxyUrl, { const response = await unsentRequest.fetchWithTimeout(this.proxyUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json; charset=utf-8' }, headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ branch: this.branch, ...payload }), body: JSON.stringify({ branch: this.branch, ...payload }),
}); });
const json = await response.json(); const json = await response.json();
if (response.ok) { if (response.ok) {
return json; return json;
} else { } else {

View File

@ -1,6 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
import { createEntry } from 'ValueObjects/Entry'; import { createEntry } from 'ValueObjects/Entry';
import { selectEntrySlug } from 'Reducers/collections'; import { selectEntrySlug } from 'Reducers/collections';
import { unsentRequest } from 'netlify-cms-lib-util';
const { fetchWithTimeout: fetch } = unsentRequest;
function getSlug(path) { function getSlug(path) {
return path return path

View File

@ -1,5 +1,8 @@
import { pickBy, trimEnd } from 'lodash'; import { pickBy, trimEnd } from 'lodash';
import { addParams } from 'Lib/urlHelper'; import { addParams } from 'Lib/urlHelper';
import { unsentRequest } from 'netlify-cms-lib-util';
const { fetchWithTimeout: fetch } = unsentRequest;
export default class AssetStore { export default class AssetStore {
constructor(config, getToken) { constructor(config, getToken) {

View File

@ -3,6 +3,27 @@ import curry from 'lodash/curry';
import flow from 'lodash/flow'; import flow from 'lodash/flow';
import isString from 'lodash/isString'; import isString from 'lodash/isString';
let controller;
let signal;
if (typeof window !== 'undefined') {
controller = window.AbortController && new AbortController();
signal = controller && controller.signal;
}
const timeout = 60;
const fetchWithTimeout = (input, init) => {
if (controller && signal && !init.signal) {
setTimeout(() => controller.abort(), timeout * 1000);
return fetch(input, { ...init, signal }).catch(e => {
if (e.name === 'AbortError') {
throw new Error(`Request timed out after ${timeout} seconds`);
}
throw e;
});
}
return fetch(input, init);
};
const decodeParams = paramsString => const decodeParams = paramsString =>
List(paramsString.split('&')) List(paramsString.split('&'))
.map(s => List(s.split('=')).map(decodeURIComponent)) .map(s => List(s.split('=')).map(decodeURIComponent))
@ -51,7 +72,7 @@ const ensureRequestArg2 = func => (arg, req) => func(arg, maybeRequestArg(req));
// This actually performs the built request object // This actually performs the built request object
const performRequest = ensureRequestArg(req => { const performRequest = ensureRequestArg(req => {
const args = toFetchArguments(req); const args = toFetchArguments(req);
return fetch(...args); return fetchWithTimeout(...args);
}); });
// Each of the following functions takes options and returns another // Each of the following functions takes options and returns another
@ -91,4 +112,5 @@ export default {
withParams, withParams,
withRoot, withRoot,
withNoCache, withNoCache,
fetchWithTimeout,
}; };