diff --git a/extensions/debugging/package.json b/extensions/debugging/package.json index 3f8640c7e81..40ed709b43a 100644 --- a/extensions/debugging/package.json +++ b/extensions/debugging/package.json @@ -28,13 +28,12 @@ }, "peerDependencies": { "@ohif/core": "^2.6.0", - "dicom-parser": "^1.8.3", - "dicomweb-client": "^0.5.2" + "dicom-parser": "^1.8.3" }, "dependencies": { "@babel/runtime": "^7.5.5", "detect-browser": "5.1.1", - "dicomweb-client": "^0.6.0", + "dicomweb-client": "^0.8.1", "file-saver": "^2.0.2", "jszip": "^3.2.2" } diff --git a/extensions/dicom-tag-browser/package.json b/extensions/dicom-tag-browser/package.json index c111bb25d44..9c223045833 100644 --- a/extensions/dicom-tag-browser/package.json +++ b/extensions/dicom-tag-browser/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@babel/runtime": "^7.5.5", - "dicomweb-client": "^0.6.0", + "dicomweb-client": "^0.8.1", "moment": "2.24.0", "react-select": "^3.0.8" } diff --git a/platform/core/package.json b/platform/core/package.json index df6beed5fb6..fd3ca9c4323 100644 --- a/platform/core/package.json +++ b/platform/core/package.json @@ -40,12 +40,13 @@ "@babel/runtime": "^7.5.5", "ajv": "^6.10.0", "dcmjs": "0.18.8", - "dicomweb-client": "^0.6.0", + "dicomweb-client": "^0.8.1", "immer": "6.0.2", "isomorphic-base64": "^1.0.2", "lodash.clonedeep": "^4.5.0", "lodash.merge": "^4.6.1", "mousetrap": "^1.6.3", + "retry": "^0.12.0", "validate.js": "^0.12.0" } } diff --git a/platform/core/src/DICOMSR/handleStructuredReport.js b/platform/core/src/DICOMSR/handleStructuredReport.js index fcaa19890b4..5bb8180387e 100644 --- a/platform/core/src/DICOMSR/handleStructuredReport.js +++ b/platform/core/src/DICOMSR/handleStructuredReport.js @@ -6,6 +6,7 @@ import parseDicomStructuredReport from './parseDicomStructuredReport'; import parseMeasurementsData from './parseMeasurementsData'; import getAllDisplaySets from './utils/getAllDisplaySets'; import errorHandler from '../errorHandler'; +import getXHRRetryRequestHook from '../utils/xhrRetryRequestHook'; const VERSION_NAME = 'dcmjs-0.0'; const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1'; @@ -23,6 +24,7 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => { url: serverUrl, headers: DICOMWeb.getAuthorizationHeader(), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); @@ -74,6 +76,7 @@ const stowSRFromMeasurements = async (measurements, serverUrl) => { url: serverUrl, headers: DICOMWeb.getAuthorizationHeader(), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); diff --git a/platform/core/src/classes/metadata/StudyMetadata.js b/platform/core/src/classes/metadata/StudyMetadata.js index 1928322144e..e2e682cb2c6 100644 --- a/platform/core/src/classes/metadata/StudyMetadata.js +++ b/platform/core/src/classes/metadata/StudyMetadata.js @@ -12,6 +12,7 @@ import { isImage } from '../../utils/isImage'; import { isDisplaySetReconstructable, isSpacingUniform } from '../../utils/isDisplaySetReconstructable'; import errorHandler from '../../errorHandler'; import isLowPriorityModality from '../../utils/isLowPriorityModality'; +import getXHRRetryRequestHook from '../../utils/xhrRetryRequestHook'; class StudyMetadata extends Metadata { constructor(data, uid) { @@ -939,6 +940,7 @@ function _getDisplaySetFromSopClassModule( url: study.getData().wadoRoot, headers, errorInterceptor, + requestHooks: [getXHRRetryRequestHook()], }); let displaySet = plugin.getDisplaySetFromSeries( diff --git a/platform/core/src/studies/services/qido/studies.js b/platform/core/src/studies/services/qido/studies.js index 93cb292d4d2..7a66c88f4c8 100644 --- a/platform/core/src/studies/services/qido/studies.js +++ b/platform/core/src/studies/services/qido/studies.js @@ -2,6 +2,7 @@ import { api } from 'dicomweb-client'; import DICOMWeb from '../../../DICOMWeb/'; import errorHandler from '../../../errorHandler'; +import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook'; /** * Creates a QIDO date string for a date range query @@ -118,6 +119,7 @@ export default function Studies(server, filter) { url: server.qidoRoot, headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js b/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js index 81585772b67..8e88302391a 100644 --- a/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js +++ b/platform/core/src/studies/services/wado/retrieveMetadataLoaderAsync.js @@ -10,6 +10,7 @@ import { } from './studyInstanceHelpers'; import errorHandler from '../../../errorHandler'; +import { getXHRRetryRequestHook } from '../../../utils/xhrRetryRequestHook'; const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary; @@ -76,6 +77,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader url: server.qidoRoot, headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }); this.client = client; diff --git a/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js b/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js index 90d1ae98842..eebea975e01 100644 --- a/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js +++ b/platform/core/src/studies/services/wado/retrieveMetadataLoaderSync.js @@ -4,6 +4,7 @@ import { createStudyFromSOPInstanceList } from './studyInstanceHelpers'; import RetrieveMetadataLoader from './retrieveMetadataLoader'; import errorHandler from '../../../errorHandler'; +import getXHRRetryRequestHook from '../../../utils/xhrRetryRequestHook'; /** * Class for sync load of study metadata. @@ -61,6 +62,7 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader { url: server.wadoRoot, headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }); this.client = client; diff --git a/platform/core/src/utils/dicomLoaderService.js b/platform/core/src/utils/dicomLoaderService.js index f0792129a2a..b354c61fffa 100644 --- a/platform/core/src/utils/dicomLoaderService.js +++ b/platform/core/src/utils/dicomLoaderService.js @@ -4,6 +4,7 @@ import { api } from 'dicomweb-client'; import DICOMWeb from '../DICOMWeb'; import errorHandler from '../errorHandler'; +import getXHRRetryRequestHook from './xhrRetryRequestHook'; const getImageId = imageObj => { if (!imageObj) { @@ -66,6 +67,7 @@ const wadorsRetriever = ( url, headers, errorInterceptor, + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); diff --git a/platform/core/src/utils/index.js b/platform/core/src/utils/index.js index 6e9ea3140d5..87d6e19fd96 100644 --- a/platform/core/src/utils/index.js +++ b/platform/core/src/utils/index.js @@ -18,6 +18,7 @@ import isDicomUid from './isDicomUid'; import resolveObjectPath from './resolveObjectPath'; import * as hierarchicalListUtils from './hierarchicalListUtils'; import * as progressTrackingUtils from './progressTrackingUtils'; +import xhrRetryRequestHook from './xhrRetryRequestHook'; const utils = { guid, @@ -40,6 +41,7 @@ const utils = { resolveObjectPath, hierarchicalListUtils, progressTrackingUtils, + xhrRetryRequestHook, }; export { @@ -63,6 +65,7 @@ export { resolveObjectPath, hierarchicalListUtils, progressTrackingUtils, + xhrRetryRequestHook, }; export default utils; diff --git a/platform/core/src/utils/index.test.js b/platform/core/src/utils/index.test.js index 85eb382304a..a3ea78aa304 100644 --- a/platform/core/src/utils/index.test.js +++ b/platform/core/src/utils/index.test.js @@ -23,6 +23,7 @@ describe('Top level exports', () => { 'resolveObjectPath', 'hierarchicalListUtils', 'progressTrackingUtils', + 'xhrRetryRequestHook', ].sort(); const exports = Object.keys(utils.default).sort(); diff --git a/platform/core/src/utils/metadataProvider/fetchOverlayData.js b/platform/core/src/utils/metadataProvider/fetchOverlayData.js index 11e4d99cda7..42af12e0447 100644 --- a/platform/core/src/utils/metadataProvider/fetchOverlayData.js +++ b/platform/core/src/utils/metadataProvider/fetchOverlayData.js @@ -4,6 +4,7 @@ import str2ab from '../str2ab'; import unpackOverlay from './unpackOverlay'; import errorHandler from '../../errorHandler'; +import getXHRRetryRequestHook from '../xhrRetryRequestHook'; export default async function fetchOverlayData(instance, server) { const OverlayDataPromises = []; @@ -69,6 +70,7 @@ async function _getOverlayData(tag, server) { url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); const options = { diff --git a/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js index 8aba64ec4b0..26c65df577d 100644 --- a/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js +++ b/platform/core/src/utils/metadataProvider/fetchPaletteColorLookupTableData.js @@ -3,6 +3,7 @@ import DICOMWeb from '../../DICOMWeb'; import str2ab from '../str2ab'; import errorHandler from '../../errorHandler'; +import getXHRRetryRequestHook from '../xhrRetryRequestHook'; export default async function fetchPaletteColorLookupTableData( instance, @@ -141,6 +142,7 @@ function _getPaletteColor(server, paletteColorLookupTableData, lutDescriptor) { url: server.wadoRoot, //BulkDataURI is absolute, so this isn't used headers: DICOMWeb.getAuthorizationHeader(server), errorInterceptor: errorHandler.getHTTPErrorHandler(), + requestHooks: [getXHRRetryRequestHook()], }; const dicomWeb = new api.DICOMwebClient(config); const options = { diff --git a/platform/core/src/utils/xhrRetryRequestHook.js b/platform/core/src/utils/xhrRetryRequestHook.js new file mode 100644 index 00000000000..2b492971da6 --- /dev/null +++ b/platform/core/src/utils/xhrRetryRequestHook.js @@ -0,0 +1,103 @@ +import retry from 'retry'; + +const defaultRetryOptions = { + retries: 5, + factor: 3, + minTimeout: 1 * 1000, + maxTimeout: 60 * 1000, + randomize: true, + retryableStatusCodes: [429, 500], +}; + +let retryOptions = { ...defaultRetryOptions }; + +/** + * Request hook used to add retry functionality to XHR requests. + * + * @param {XMLHttpRequest} request XHR request instance + * @param {object} metadata Metadata about the request + * @param {object} metadata.url URL + * @param {object} metadata.method HTTP method + * @returns {XMLHttpRequest} request instance optionally modified + */ +const xhrRetryRequestHook = (request, metadata) => { + const { url, method } = metadata; + + function faultTolerantRequestSend(...args) { + const operation = retry.operation(retryOptions); + + operation.attempt(function operationAttempt(currentAttempt) { + const originalOnReadyStateChange = request.onreadystatechange; + + /** Overriding/extending XHR function */ + request.onreadystatechange = function onReadyStateChange(...args) { + originalOnReadyStateChange.apply(request, args); + + if (retryOptions.retryableStatusCodes.includes(request.status)) { + const errorMessage = `Attempt to request ${url} failed.`; + const attemptFailedError = new Error(errorMessage); + operation.retry(attemptFailedError); + } + }; + + /** Call open only on retry (after headers and other things were set in the xhr instance) */ + if (currentAttempt > 1) { + console.warn(`Requesting ${url}... (attempt: ${currentAttempt})`); + request.open(method, url, true); + } + }); + + originalRequestSend.apply(request, args); + } + + /** Overriding/extending XHR function */ + const originalRequestSend = request.send; + request.send = faultTolerantRequestSend; + + return request; +}; + +/** + * Returns a configured retry request hook function + * that can be used to add retry functionality to XHR request. + * + * Default options: + * retries: 5 + * factor: 3 + * minTimeout: 1 * 1000 + * maxTimeout: 60 * 1000 + * randomize: true + * + * @param {object} options + * @param {number} options.retires number of retries + * @param {number} options.factor factor + * @param {number} options.minTimeout the min timeout + * @param {number} options.maxTimeout the max timeout + * @param {boolean} options.randomize randomize + * @param {array} options.retryableStatusCodes status codes that can trigger retry + * @returns {function} the configured retry request function + */ +export const getXHRRetryRequestHook = (options = {}) => { + retryOptions = { ...defaultRetryOptions }; + if ('retries' in options) { + retryOptions.retries = options.retries; + } + if ('factor' in options) { + retryOptions.factor = options.factor; + } + if ('minTimeout' in options) { + retryOptions.minTimeout = options.minTimeout; + } + if ('maxTimeout' in options) { + retryOptions.maxTimeout = options.maxTimeout; + } + if ('randomize' in options) { + retryOptions.randomize = options.randomize; + } + if ('retryableStatusCodes' in options) { + retryOptions.retryableStatusCodes = options.retryableStatusCodes; + } + return xhrRetryRequestHook; +}; + +export default getXHRRetryRequestHook; diff --git a/platform/viewer/package.json b/platform/viewer/package.json index 0b9f1e4f70f..924ecc5fddc 100644 --- a/platform/viewer/package.json +++ b/platform/viewer/package.json @@ -70,7 +70,7 @@ "cornerstone-wado-image-loader": "^3.1.0", "dcmjs": "0.18.8", "dicom-parser": "^1.8.3", - "dicomweb-client": "^0.4.4", + "dicomweb-client": "^0.8.1", "hammerjs": "^2.0.8", "i18next": "^17.0.3", "i18next-browser-languagedetector": "^3.0.1", diff --git a/yarn.lock b/yarn.lock index b1ab3c665fc..d439562f6c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6940,20 +6940,15 @@ dicom-parser@^1.8.3: resolved "https://registry.yarnpkg.com/dicom-parser/-/dicom-parser-1.8.3.tgz#6e4b862898112304db30143147562011c1ce6a4d" integrity sha512-CMeUr+jea7Ml70N+/Z5Pd2MYtvLp6IU+TnvdLe6VRVKzZuTeYLYyuAQa9R+sFK4v4N39hig+hKHN+Wfi9sQ6GA== -dicomweb-client@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.4.4.tgz#0ca0c7706556f3114330a3c40946ee66808148e4" - integrity sha512-5S7wLYxQgHnOyEgR1zlZlt79IjzOk+YDB/Jjzte6ijRZsAoj9RUiIZcsTyxvBD9gLMYs/954n2kYHdbz83Xbtw== - dicomweb-client@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.5.2.tgz#aa5a4a6a5044b0702bb0a8a2662c86cd16901951" integrity sha512-e11n2+g7HfBuMyopWq76W11eSZJ707g4U1XiO8URA+23aoe0g1kGAtlPB29NKXl7zX7RznTJ7wfRBxFkW6EJDA== -dicomweb-client@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.6.0.tgz#5e35ada52fe0155af1cc1f0e84f9c7f76477f92d" - integrity sha512-VAkBg4W6odIo2XsFxqjN/rptd7bQ8oHpRuKH5d46E9BUIPzRschazE8Dx1xg7/l3N3f1M70jB7yJ339arAXiDQ== +dicomweb-client@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.8.1.tgz#12bdf0e9698bcf82730f38829ac30158164bb3fd" + integrity sha512-5EbK8epfSuonSnCW3nr2tkA1CcO1aFT3grEVvKXqv0tVyHsBvbEcEmBcPEbu1m/cF6t7+3uQRLOQ4dw4CodxFg== diff-sequences@^24.9.0: version "24.9.0"