diff --git a/packages/common/src/dto/api/index.ts b/packages/common/src/dto/api/index.ts index a7137171f..ecf069930 100644 --- a/packages/common/src/dto/api/index.ts +++ b/packages/common/src/dto/api/index.ts @@ -125,6 +125,7 @@ export interface IServerConfig { inactivityTimeout: number; runTimeout: number; startTimeout: number; + axiosRequestTimeout: number; }; networking?: { auth?: { diff --git a/packages/dashboard-backend/src/constants/server-config.ts b/packages/dashboard-backend/src/constants/server-config.ts index c04c26cc1..1ac0abeca 100644 --- a/packages/dashboard-backend/src/constants/server-config.ts +++ b/packages/dashboard-backend/src/constants/server-config.ts @@ -11,3 +11,4 @@ */ export const startTimeoutSeconds = 300; // 5 minutes +export const requestTimeoutSeconds = 30; // 30 seconds diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts index 3f867128e..f77dc2e8a 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/__tests__/serverConfigApi.spec.ts @@ -165,6 +165,18 @@ describe('Server Config API Service', () => { const res = serverConfigService.getAllowedSourceUrls(buildCustomResource()); expect(res).toEqual(['https://github.com']); }); + + describe('Axios Request Timeout', () => { + test('getting default value', () => { + const res = serverConfigService.getAxiosRequestTimeout(); + expect(res).toEqual(30000); + }); + test('getting custom value', () => { + process.env['CHE_DASHBOARD_AXIOS_REQUEST_TIMEOUT'] = '55000'; + const res = serverConfigService.getAxiosRequestTimeout(); + expect(res).toEqual(55000); + }); + }); }); function buildCustomResourceList(): { body: CustomResourceDefinitionList } { diff --git a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts index e212ff298..409a4ecb8 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/services/serverConfigApi.ts @@ -16,7 +16,7 @@ import * as k8s from '@kubernetes/client-node'; import { readFileSync } from 'fs'; import path from 'path'; -import { startTimeoutSeconds } from '@/constants/server-config'; +import { requestTimeoutSeconds, startTimeoutSeconds } from '@/constants/server-config'; import { createError } from '@/devworkspaceClient/services/helpers/createError'; import { CustomObjectAPI, @@ -213,6 +213,17 @@ export class ServerConfigApiService implements IServerConfigApi { return cheCustomResource.spec.devEnvironments?.startTimeoutSeconds || startTimeoutSeconds; } + getAxiosRequestTimeout(): number { + const requestTimeoutStr = process.env['CHE_DASHBOARD_AXIOS_REQUEST_TIMEOUT']; + if (requestTimeoutStr === undefined) { + return requestTimeoutSeconds * 1000; + } + + const requestTimeout = parseInt(requestTimeoutStr, 10); + + return isNaN(requestTimeout) ? requestTimeoutSeconds * 1000 : requestTimeout; + } + getDashboardLogo( cheCustomResource: CheClusterCustomResource, ): { base64data: string; mediatype: string } | undefined { diff --git a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts index 8c0d99eb9..86eb5ba80 100644 --- a/packages/dashboard-backend/src/devworkspaceClient/types/index.ts +++ b/packages/dashboard-backend/src/devworkspaceClient/types/index.ts @@ -223,6 +223,7 @@ export function isCheClusterCustomResourceSpecDevEnvironments( export type CheClusterCustomResourceSpecComponents = { cheServer?: Record; dashboard?: { + requestTimeout?: number; branding?: { logo?: { base64data: string; @@ -347,6 +348,11 @@ export interface IServerConfigApi { */ getWorkspaceStartTimeout(cheCustomResource: CheClusterCustomResource): number; + /** + * Returns the axios request timeout + */ + getAxiosRequestTimeout(): number; + /** * Returns the dashboard branding logo */ diff --git a/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts b/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts index 482173d68..e30e9c735 100644 --- a/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts +++ b/packages/dashboard-backend/src/routes/api/__tests__/serverConfig.spec.ts @@ -44,7 +44,12 @@ describe('Server Config Route', () => { }, defaults: { components: [], plugins: [], pvcStrategy: '' }, pluginRegistry: { openVSXURL: 'openvsx-url' }, - timeouts: { inactivityTimeout: 0, runTimeout: 0, startTimeout: 0 }, + timeouts: { + inactivityTimeout: 0, + runTimeout: 0, + startTimeout: 0, + axiosRequestTimeout: 0, + }, networking: { auth: { advancedAuthorization: {}, diff --git a/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts b/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts index 424af1ef3..f6e8ffb2e 100644 --- a/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts +++ b/packages/dashboard-backend/src/routes/api/helpers/__mocks__/getDevWorkspaceClient.ts @@ -55,6 +55,7 @@ export const stubAllWorkspacesLimit = 1; export const stubWorkspaceInactivityTimeout = 0; export const stubWorkspaceRunTimeout = 0; export const stubWorkspaceStartupTimeout = 0; +export const stubAxiosRequestTimeout = 0; export const defaultPluginRegistryUrl = 'http://plugin-registry.eclipse-che.svc/v3'; export const internalRegistryDisableStatus = true; export const externalDevfileRegistries = [{ url: 'https://devfile.registry.test.org/' }]; @@ -181,6 +182,7 @@ export const getDevWorkspaceClient = jest.fn( getWorkspaceInactivityTimeout: _cheCustomResource => stubWorkspaceInactivityTimeout, getWorkspaceRunTimeout: _cheCustomResource => stubWorkspaceRunTimeout, getWorkspaceStartTimeout: _cheCustomResource => stubWorkspaceStartupTimeout, + getAxiosRequestTimeout: () => stubAxiosRequestTimeout, getDefaultPluginRegistryUrl: _cheCustomResource => defaultPluginRegistryUrl, getExternalDevfileRegistries: _cheCustomResource => externalDevfileRegistries, getInternalRegistryDisableStatus: _cheCustomResource => internalRegistryDisableStatus, diff --git a/packages/dashboard-backend/src/routes/api/serverConfig.ts b/packages/dashboard-backend/src/routes/api/serverConfig.ts index b2118554c..52c36a1c2 100644 --- a/packages/dashboard-backend/src/routes/api/serverConfig.ts +++ b/packages/dashboard-backend/src/routes/api/serverConfig.ts @@ -48,6 +48,7 @@ export function registerServerConfigRoute(instance: FastifyInstance) { const advancedAuthorization = serverConfigApi.getAdvancedAuthorization(cheCustomResource); const autoProvision = serverConfigApi.getAutoProvision(cheCustomResource); const allowedSourceUrls = serverConfigApi.getAllowedSourceUrls(cheCustomResource); + const axiosRequestTimeout = serverConfigApi.getAxiosRequestTimeout(); const serverConfig: api.IServerConfig = { containerBuild, @@ -61,6 +62,7 @@ export function registerServerConfigRoute(instance: FastifyInstance) { inactivityTimeout, runTimeout, startTimeout, + axiosRequestTimeout, }, devfileRegistry: { disableInternalRegistry, diff --git a/packages/dashboard-frontend/src/__tests__/const.ts b/packages/dashboard-frontend/src/__tests__/const.ts index 6fd3bf48c..3c3eeaf59 100644 --- a/packages/dashboard-frontend/src/__tests__/const.ts +++ b/packages/dashboard-frontend/src/__tests__/const.ts @@ -98,6 +98,7 @@ export const serverConfigData = { inactivityTimeout: 10800, runTimeout: 86400, startTimeout: 300, + axiosRequestTimeout: 30000, }, devfileRegistry: { disableInternalRegistry: false, diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx index 9143bd215..4dcc97a45 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/StartingSteps/StartWorkspace/__tests__/index.spec.tsx @@ -76,6 +76,7 @@ const serverConfig: api.IServerConfig = { inactivityTimeout: -1, runTimeout: -1, startTimeout, + axiosRequestTimeout: 30000, }, defaultNamespace: { autoProvision: true, diff --git a/packages/dashboard-frontend/src/services/axios-wrapper/getAxiosInstance.ts b/packages/dashboard-frontend/src/services/axios-wrapper/getAxiosInstance.ts index af8c1a063..2f6728980 100644 --- a/packages/dashboard-frontend/src/services/axios-wrapper/getAxiosInstance.ts +++ b/packages/dashboard-frontend/src/services/axios-wrapper/getAxiosInstance.ts @@ -17,7 +17,8 @@ class CheAxiosInstance { private readonly axiosInstance: AxiosInstance; private constructor() { - this.axiosInstance = axios.create({ timeout: 30000 }); + this.axiosInstance = axios.create(); + this.axiosInstance.defaults.timeout = 15000; } public static getInstance(): CheAxiosInstance { diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/actions.spec.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/actions.spec.ts index 0ea88a98c..7c4cc26bc 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/actions.spec.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/actions.spec.ts @@ -12,6 +12,7 @@ import common, { api } from '@eclipse-che/common'; +import { getAxiosInstance } from '@/services/axios-wrapper/getAxiosInstance'; import * as ServerConfigApi from '@/services/backend-client/serverConfigApi'; import { createMockStore } from '@/store/__mocks__/mockActionsTestStore'; import { verifyAuthorized } from '@/store/SanityCheck'; @@ -38,14 +39,21 @@ describe('ServerConfig, actions', () => { it('should dispatch receive action on successful fetch', async () => { const mockConfig = { allowedSourceUrls: ['https://github.com'], + timeouts: { + axiosRequestTimeout: 30000, + }, // ... } as api.IServerConfig; - (verifyAuthorized as jest.Mock).mockResolvedValue(true); + expect(getAxiosInstance().defaults.timeout).toEqual(15000); + (ServerConfigApi.fetchServerConfig as jest.Mock).mockResolvedValue(mockConfig); await store.dispatch(actionCreators.requestServerConfig()); + expect(getAxiosInstance().defaults.timeout).toEqual(30000); + expect(verifyAuthorized).not.toHaveBeenCalled(); + const actions = store.getActions(); expect(actions).toHaveLength(2); expect(actions[0]).toEqual(serverConfigRequestAction()); @@ -55,14 +63,16 @@ describe('ServerConfig, actions', () => { it('should dispatch error action on failed fetch', async () => { const errorMessage = 'Network error'; - (verifyAuthorized as jest.Mock).mockResolvedValue(true); (ServerConfigApi.fetchServerConfig as jest.Mock).mockRejectedValue(new Error(errorMessage)); (common.helpers.errors.getMessage as jest.Mock).mockReturnValue(errorMessage); + (verifyAuthorized as jest.Mock).mockResolvedValue(true); await expect(store.dispatch(actionCreators.requestServerConfig())).rejects.toThrow( `Failed to fetch workspace defaults. ${errorMessage}`, ); + expect(verifyAuthorized).toHaveBeenCalled(); + const actions = store.getActions(); expect(actions).toHaveLength(2); expect(actions[0]).toEqual(serverConfigRequestAction()); diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/reducer.spec.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/reducer.spec.ts index 9ba00effd..157590d70 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/reducer.spec.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/reducer.spec.ts @@ -66,6 +66,7 @@ describe('ServerConfig, reducer', () => { inactivityTimeout: 100, runTimeout: 200, startTimeout: 300, + axiosRequestTimeout: 30000, }, defaultNamespace: { autoProvision: false, diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts index 4fafb4697..508b9210b 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/selectors.spec.ts @@ -64,6 +64,7 @@ describe('ServerConfig Selectors', () => { pluginRegistryInternalURL: 'https://internal.plugin.registry', timeouts: { startTimeout: 300, + axiosRequestTimeout: 30000, }, dashboardLogo: { base64data: 'base64data', diff --git a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts index e8b7aee51..3b15f0dd3 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/__tests__/stubs.ts @@ -44,6 +44,7 @@ export const serverConfig: api.IServerConfig = { inactivityTimeout: -1, runTimeout: -1, startTimeout: 300, + axiosRequestTimeout: 30000, }, defaultNamespace: { autoProvision: true, diff --git a/packages/dashboard-frontend/src/store/ServerConfig/actions.ts b/packages/dashboard-frontend/src/store/ServerConfig/actions.ts index 365060bfc..4a63fa54a 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/actions.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/actions.ts @@ -13,6 +13,7 @@ import common, { api } from '@eclipse-che/common'; import { createAction } from '@reduxjs/toolkit'; +import { getAxiosInstance } from '@/services/axios-wrapper/getAxiosInstance'; import * as ServerConfigApi from '@/services/backend-client/serverConfigApi'; import { AppThunk } from '@/store'; import { verifyAuthorized } from '@/store/SanityCheck'; @@ -24,14 +25,17 @@ export const serverConfigErrorAction = createAction('serverConfig/error' export const actionCreators = { requestServerConfig: (): AppThunk => async (dispatch, getState) => { try { - await verifyAuthorized(dispatch, getState); - dispatch(serverConfigRequestAction()); const config = await ServerConfigApi.fetchServerConfig(); + if (config?.timeouts?.axiosRequestTimeout) { + getAxiosInstance().defaults.timeout = config.timeouts.axiosRequestTimeout; + } + dispatch(serverConfigReceiveAction(config)); } catch (e) { + await verifyAuthorized(dispatch, getState); const error = common.helpers.errors.getMessage(e); dispatch(serverConfigErrorAction(error)); throw new Error(`Failed to fetch workspace defaults. ${error}`); diff --git a/packages/dashboard-frontend/src/store/ServerConfig/reducer.ts b/packages/dashboard-frontend/src/store/ServerConfig/reducer.ts index 0ddfb997d..2ba6aabbe 100644 --- a/packages/dashboard-frontend/src/store/ServerConfig/reducer.ts +++ b/packages/dashboard-frontend/src/store/ServerConfig/reducer.ts @@ -46,6 +46,7 @@ export const unloadedState: State = { inactivityTimeout: -1, runTimeout: -1, startTimeout: 300, + axiosRequestTimeout: 30000, }, defaultNamespace: { autoProvision: true,