diff --git a/lib/utils/fetch-utils.js b/lib/utils/fetch-utils.js new file mode 100644 index 000000000..173ad6370 --- /dev/null +++ b/lib/utils/fetch-utils.js @@ -0,0 +1,56 @@ +// @flow + +type RequestWithTimeoutOptions = { + ...$Exact, + +timeout?: number, // ms +}; + +async function fetchWithTimeout( + url: string, + options?: RequestWithTimeoutOptions, +): Promise { + const { timeout, signal: externalSignal, ...requestOptions } = options || {}; + + const abortController = new AbortController(); + + // Handle situation when callee has abort signal already set + let externalAbort = false; + const externalAbortEvent = () => { + externalAbort = true; + abortController.abort(); + }; + if (externalSignal) { + if (externalSignal.aborted) { + externalAbortEvent(); + } else { + externalSignal.addEventListener('abort', externalAbortEvent); + } + } + + let timeoutHandle; + if (timeout) { + timeoutHandle = setTimeout(() => { + abortController.abort(); + }, timeout); + } + + try { + return await fetch(url, { + ...requestOptions, + signal: abortController.signal, + }); + } catch (err) { + if (abortController.signal.aborted && !externalAbort) { + throw new Error('Request timed out'); + } else { + throw err; + } + } finally { + clearTimeout(timeoutHandle); + if (externalSignal) { + externalSignal.removeEventListener('abort', externalAbortEvent); + } + } +} + +export { fetchWithTimeout }; diff --git a/lib/utils/reports-service.js b/lib/utils/reports-service.js index deb79eeb7..f8097aca6 100644 --- a/lib/utils/reports-service.js +++ b/lib/utils/reports-service.js @@ -1,50 +1,54 @@ // @flow +import { fetchWithTimeout } from './fetch-utils.js'; import { reportsServiceURL, sendReportEndpoint, } from '../facts/reports-service.js'; import type { ReportsServiceSendReportsRequest, ReportsServiceSendReportsResponse, } from '../types/report-types.js'; +const REQUEST_TIMEOUT = 60 * 1000; // 60s + async function sendReports( reports: ReportsServiceSendReportsRequest, ): Promise { const reportsArray = Array.isArray(reports) ? reports : [reports]; if (reportsArray.length === 0) { return { reportIDs: [] }; } const requestBody = reportsArray.map(clientReport => { const { id, ...report } = clientReport; return report; }); const url = `${reportsServiceURL}${sendReportEndpoint.path}`; - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: sendReportEndpoint.method, body: JSON.stringify(requestBody), headers: { 'Content-Type': 'application/json', }, + timeout: REQUEST_TIMEOUT, }); if (!response.ok) { const { status, statusText } = response; let responseText, errorMessage; try { responseText = await response.text(); } finally { errorMessage = responseText || statusText || '-'; } // we cannot throw error inside `finally` block because eslint complains throw new Error(`Server responded with HTTP ${status}: ${errorMessage}`); } const { reportIDs } = await response.json(); return { reportIDs }; } export { sendReports };