From 85bc3675b7fed476cda3777db194e71c84373af4 Mon Sep 17 00:00:00 2001 From: Roman Donchenko Date: Mon, 4 Dec 2023 13:19:01 +0200 Subject: [PATCH 1/2] cvat-core: retry HTTP requests if a 429 status is returned This should help reduce breakage if a user hits an API rate limit. --- .../20231204_141903_roman_ui_retry_429.md | 4 ++ cvat-core/package.json | 1 + cvat-core/src/server-proxy.ts | 14 +++++++ yarn.lock | 42 +++++++++++++------ 4 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 changelog.d/20231204_141903_roman_ui_retry_429.md diff --git a/changelog.d/20231204_141903_roman_ui_retry_429.md b/changelog.d/20231204_141903_roman_ui_retry_429.md new file mode 100644 index 000000000000..2466ad47e13b --- /dev/null +++ b/changelog.d/20231204_141903_roman_ui_retry_429.md @@ -0,0 +1,4 @@ +### Added + +- The UI will now retry requests that were rejected due to rate limiting + () diff --git a/cvat-core/package.json b/cvat-core/package.json index a9ff302733f1..b0c6992ed1c0 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -30,6 +30,7 @@ "dependencies": { "@types/lodash": "^4.14.191", "axios": "^1.6.0", + "axios-retry": "^4.0.0", "cvat-data": "link:./../cvat-data", "detect-browser": "^5.2.1", "error-stack-parser": "^2.0.2", diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 610dfa479e21..6f902edad84c 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -6,6 +6,7 @@ import FormData from 'form-data'; import store from 'store'; import Axios, { AxiosError, AxiosResponse } from 'axios'; +import axiosRetry from 'axios-retry'; import * as tus from 'tus-js-client'; import { ChunkQuality } from 'cvat-data'; @@ -284,6 +285,19 @@ Axios.interceptors.response.use((response) => { return response; }); +axiosRetry(Axios, { + retryCondition: (error) => error.response && error.response.status === Axios.HttpStatusCode.TooManyRequests, + retryDelay: (retryCount, error) => { + const retryAfterValue = error.response.headers['retry-after']; + + // Retry-After is allowed to be a number as well as a date. + // We're probably not going to use the latter option, though, so don't bother parsing it. + if (!retryAfterValue || !/^\d+$/.test(retryAfterValue)) return axiosRetry.exponentialDelay(retryCount, error); + + return parseInt(retryAfterValue, 10) * 1000; + }, +}); + let token = store.get('token'); if (token) { Axios.defaults.headers.common.Authorization = `Token ${token}`; diff --git a/yarn.lock b/yarn.lock index facbf1a72eb7..635bc99f9574 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2227,11 +2227,6 @@ resolved "https://registry.yarnpkg.com/@types/polylabel/-/polylabel-1.1.3.tgz#15aba4277b03ac0ab60a0dea75a13bde45dcfc01" integrity sha512-9Zw2KoDpi+T4PZz2G6pO2xArE0m/GSMTW1MIxF2s8ZY8x9XDO6fv9um0ydRGvcbkFLlaq8yNK6eZxnmMZtDgWQ== -"@types/prettier@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb" - integrity sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw== - "@types/prismjs@^1.0.0": version "1.26.3" resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.3.tgz#47fe8e784c2dee24fe636cab82e090d3da9b7dec" @@ -2265,12 +2260,12 @@ "@types/react" "*" "@types/reactcss" "*" -"@types/react-dom@^16.9.14", "@types/react-dom@^18.0.5": - version "18.2.17" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.17.tgz#375c55fab4ae671bd98448dcfa153268d01d6f64" - integrity sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg== +"@types/react-dom@^16.9.14": + version "16.9.24" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.24.tgz#4d193d7d011267fca842e8a10a2d738f92ec5c30" + integrity sha512-Gcmq2JTDheyWn/1eteqyzzWKSqDjYU6KYsIvH7thb7CR5OYInAWOX+7WnKf6PaU/cbdOc4szJItcDEJO7UGmfA== dependencies: - "@types/react" "*" + "@types/react" "^16" "@types/react-grid-layout@^1.3.2": version "1.3.5" @@ -2279,7 +2274,7 @@ dependencies: "@types/react" "*" -"@types/react-redux@^7.1.18", "@types/react-redux@^7.1.20", "@types/react-redux@^7.1.24": +"@types/react-redux@^7.1.18", "@types/react-redux@^7.1.20": version "7.1.31" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.31.tgz#ff914f80401488bb31d064b1d84ae525dad41b23" integrity sha512-merF9AH72krBUekQY6uObXnMsEo1xTeZy9NONNRnqSwvwVe3HtLeRvNIPaKmPDIOWPsSFE51rc2WGpPMqmuCWg== @@ -2289,7 +2284,7 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-router-dom@^5.1.9", "@types/react-router-dom@^5.3.3": +"@types/react-router-dom@^5.1.9": version "5.3.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83" integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw== @@ -2306,7 +2301,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*", "@types/react@^16.14.15", "@types/react@^17.0.30": +"@types/react@*": version "17.0.71" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.71.tgz#3673d446ad482b1564e44bf853b3ab5bcbc942c4" integrity sha512-lfqOu9mp16nmaGRrS8deS2Taqhd5Ih0o92Te5Ws6I1py4ytHBcXLqh0YIqVsViqwVI5f+haiFM6hju814BzcmA== @@ -2315,6 +2310,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^16", "@types/react@^16.14.15": + version "16.14.52" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.52.tgz#d6c31c3f69cf6ae10718d0a07a2ef6f2ebafc490" + integrity sha512-4+ZN73hgRW3Gang3QMqWjrqPPkf+lWZYiyG4uXtUbpd+7eiBDw6Gemila6rXDd8DorADupTiIERL6Mb5BQTF2w== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.10" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.10.tgz#89feb81e913ccf68ff599dbabcc823e1b49c4f7c" @@ -3217,6 +3221,13 @@ axe-core@=4.7.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios-retry@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-4.0.0.tgz#d5cb8ea1db18e05ce6f08aa5fe8b2663bba48e60" + integrity sha512-F6P4HVGITD/v4z9Lw2mIA24IabTajvpDZmKa6zq/gGwn57wN5j1P3uWrAV0+diqnW6kTM2fTqmWNfgYWGmMuiA== + dependencies: + is-retry-allowed "^2.2.0" + axios@^1.6.0: version "1.6.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2" @@ -6500,6 +6511,11 @@ is-relative-url@^2.0.0: dependencies: is-absolute-url "^2.0.0" +is-retry-allowed@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz#88f34cbd236e043e71b6932d09b0c65fb7b4d71d" + integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== + is-set@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" From 559a3b86d05fca2e946866d7e0f9fcf16e8e25a2 Mon Sep 17 00:00:00 2001 From: Roman Donchenko Date: Thu, 7 Dec 2023 14:13:34 +0200 Subject: [PATCH 2/2] Share Axios configuration between server-proxy.ts and download.worker.ts --- cvat-core/src/axios-config.ts | 25 +++++++++++++++++++++++++ cvat-core/src/download.worker.ts | 4 +--- cvat-core/src/server-proxy.ts | 18 +----------------- 3 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 cvat-core/src/axios-config.ts diff --git a/cvat-core/src/axios-config.ts b/cvat-core/src/axios-config.ts new file mode 100644 index 000000000000..565e76b134cc --- /dev/null +++ b/cvat-core/src/axios-config.ts @@ -0,0 +1,25 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import Axios from 'axios'; +import axiosRetry from 'axios-retry'; + +Axios.defaults.withCredentials = true; +Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'; +Axios.defaults.xsrfCookieName = 'csrftoken'; + +axiosRetry(Axios, { + retryCondition: (error) => error.response && error.response.status === Axios.HttpStatusCode.TooManyRequests, + retryDelay: (retryCount, error) => { + const retryAfterValue = error.response.headers['retry-after']; + + // Retry-After is allowed to be a decimal integer as well as a date. + // We're probably not going to use the latter option, though, so don't bother parsing it. + if (!retryAfterValue || !/^\d+$/.test(retryAfterValue)) { + return axiosRetry.exponentialDelay(retryCount, error); + } + + return parseInt(retryAfterValue, 10) * 1000; + }, +}); diff --git a/cvat-core/src/download.worker.ts b/cvat-core/src/download.worker.ts index d4ba0f40feca..ce3101d17b36 100644 --- a/cvat-core/src/download.worker.ts +++ b/cvat-core/src/download.worker.ts @@ -5,9 +5,7 @@ import Axios from 'axios'; -Axios.defaults.withCredentials = true; -Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'; -Axios.defaults.xsrfCookieName = 'csrftoken'; +import './axios-config'; onmessage = (e) => { Axios.get(e.data.url, e.data.config) diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 6f902edad84c..1dd734f785dd 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -6,10 +6,10 @@ import FormData from 'form-data'; import store from 'store'; import Axios, { AxiosError, AxiosResponse } from 'axios'; -import axiosRetry from 'axios-retry'; import * as tus from 'tus-js-client'; import { ChunkQuality } from 'cvat-data'; +import './axios-config'; import { SerializedLabel, SerializedAnnotationFormats, ProjectsFilter, SerializedProject, SerializedTask, TasksFilter, SerializedUser, SerializedOrganization, @@ -243,9 +243,6 @@ class WorkerWrappedAxios { } } -Axios.defaults.withCredentials = true; -Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'; -Axios.defaults.xsrfCookieName = 'csrftoken'; const workerAxios = new WorkerWrappedAxios(); Axios.interceptors.request.use((reqConfig) => { if ('params' in reqConfig && 'org' in reqConfig.params) { @@ -285,19 +282,6 @@ Axios.interceptors.response.use((response) => { return response; }); -axiosRetry(Axios, { - retryCondition: (error) => error.response && error.response.status === Axios.HttpStatusCode.TooManyRequests, - retryDelay: (retryCount, error) => { - const retryAfterValue = error.response.headers['retry-after']; - - // Retry-After is allowed to be a number as well as a date. - // We're probably not going to use the latter option, though, so don't bother parsing it. - if (!retryAfterValue || !/^\d+$/.test(retryAfterValue)) return axiosRetry.exponentialDelay(retryCount, error); - - return parseInt(retryAfterValue, 10) * 1000; - }, -}); - let token = store.get('token'); if (token) { Axios.defaults.headers.common.Authorization = `Token ${token}`;