Skip to content

Commit

Permalink
fix: Retry request when token is missed. Part 2. (#946)
Browse files Browse the repository at this point in the history
* fix: Retry request when token is missed. Part 2.

Signed-off-by: Anatolii Bazko <[email protected]>
  • Loading branch information
tolusha authored Oct 9, 2023
1 parent 56bca30 commit 1966f74
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@
*/

import { AxiosInstance } from 'axios';
import { AxiosWrapper } from '../axiosWrapper';
import { AxiosWrapper, bearerTokenAuthorizationIsRequiredErrorMsg } from '../axiosWrapper';
import mockAxios from 'axios';

// mute console logs
console.log = jest.fn();
console.warn = jest.fn();

const errorMessage = 'Bearer Token Authorization is required';

describe('axiosWrapper', () => {
let axiosInstance: AxiosInstance;
let axiosGetMock: jest.Mock;
Expand All @@ -32,7 +30,20 @@ describe('axiosWrapper', () => {
axiosGetSpy = jest.spyOn(axiosInstance, 'get');
});

it('should retry 0 time', async () => {
it('should retry 0 time with Bearer Token Authorization is required error message', async () => {
const expectedData = { data: 'some-data' };
axiosGetMock.mockReturnValue(new Promise(resolve => resolve(expectedData)));

const result = await new AxiosWrapper(
axiosInstance,
bearerTokenAuthorizationIsRequiredErrorMsg,
).get('some-url');

expect(result).toEqual(expectedData);
expect(axiosGetSpy).toBeCalledTimes(1);
});

it('should retry 0 time without specifi error message', async () => {
const expectedData = { data: 'some-data' };
axiosGetMock.mockReturnValue(new Promise(resolve => resolve(expectedData)));

Expand All @@ -42,10 +53,25 @@ describe('axiosWrapper', () => {
expect(axiosGetSpy).toBeCalledTimes(1);
});

it('should retry 1 time', async () => {
it('should retry 1 time with Bearer Token Authorization is required error message', async () => {
const expectedData = { data: 'some-data' };
axiosGetMock
.mockRejectedValueOnce(new Error(bearerTokenAuthorizationIsRequiredErrorMsg))
.mockReturnValue(new Promise(resolve => resolve(expectedData)));

const result = await new AxiosWrapper(
axiosInstance,
bearerTokenAuthorizationIsRequiredErrorMsg,
).get('some-url');

expect(result).toEqual(expectedData);
expect(axiosGetSpy).toBeCalledTimes(2);
});

it('should retry 1 time without specif error message', async () => {
const expectedData = { data: 'some-data' };
axiosGetMock
.mockRejectedValueOnce(new Error(errorMessage))
.mockRejectedValueOnce(new Error('some error message'))
.mockReturnValue(new Promise(resolve => resolve(expectedData)));

const result = await new AxiosWrapper(axiosInstance).get('some-url');
Expand All @@ -54,11 +80,27 @@ describe('axiosWrapper', () => {
expect(axiosGetSpy).toBeCalledTimes(2);
});

it('should retry 2 times', async () => {
it('should retry 2 times with Bearer Token Authorization is required error message', async () => {
const expectedData = { data: 'some-data' };
axiosGetMock
.mockRejectedValueOnce(new Error(errorMessage))
.mockRejectedValueOnce(new Error(errorMessage))
.mockRejectedValueOnce(new Error(bearerTokenAuthorizationIsRequiredErrorMsg))
.mockRejectedValueOnce(new Error(bearerTokenAuthorizationIsRequiredErrorMsg))
.mockReturnValue(new Promise(resolve => resolve(expectedData)));

const result = await new AxiosWrapper(
axiosInstance,
bearerTokenAuthorizationIsRequiredErrorMsg,
).get('some-url');

expect(result).toEqual(expectedData);
expect(axiosGetSpy).toBeCalledTimes(3);
});

it('should retry 2 times without specific error message', async () => {
const expectedData = { data: 'some-data' };
axiosGetMock
.mockRejectedValueOnce(new Error('error 1'))
.mockRejectedValueOnce(new Error('error 2'))
.mockReturnValue(new Promise(resolve => resolve(expectedData)));

const result = await new AxiosWrapper(axiosInstance).get('some-url');
Expand All @@ -67,18 +109,36 @@ describe('axiosWrapper', () => {
expect(axiosGetSpy).toBeCalledTimes(3);
});

it('should fail after 3 times', async () => {
it('should fail after 3 times with Bearer Token Authorization is required error message', async () => {
axiosGetMock
.mockRejectedValueOnce(new Error(bearerTokenAuthorizationIsRequiredErrorMsg))
.mockRejectedValueOnce(new Error(bearerTokenAuthorizationIsRequiredErrorMsg))
.mockRejectedValueOnce(new Error(bearerTokenAuthorizationIsRequiredErrorMsg))
.mockRejectedValue(new Error(bearerTokenAuthorizationIsRequiredErrorMsg));

try {
await new AxiosWrapper(axiosInstance, bearerTokenAuthorizationIsRequiredErrorMsg).get(
'some-url',
);
fail('should fail');
} catch (e: any) {
expect(e.message).toEqual(bearerTokenAuthorizationIsRequiredErrorMsg);
expect(axiosGetSpy).toBeCalledTimes(4);
}
});

it('should fail after 3 times without specific error message', async () => {
axiosGetMock
.mockRejectedValueOnce(new Error(errorMessage))
.mockRejectedValueOnce(new Error(errorMessage))
.mockRejectedValueOnce(new Error(errorMessage))
.mockRejectedValue(new Error(errorMessage));
.mockRejectedValueOnce(new Error('error 1'))
.mockRejectedValueOnce(new Error('error 2'))
.mockRejectedValueOnce(new Error('error 3'))
.mockRejectedValue(new Error('error 4'));

try {
await new AxiosWrapper(axiosInstance).get('some-url');
fail('should fail');
} catch (e: any) {
expect(e.message).toEqual(errorMessage);
expect(e.message).toEqual('error 4');
expect(axiosGetSpy).toBeCalledTimes(4);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,20 @@ describe('Kubernetes namespace API', () => {
const namespace: che.KubernetesNamespace = { name: 'test-name', attributes: { phase: 'Active' } };

afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});

describe('fetch namespace', () => {
it('should call "/api/kubernetes/namespace"', async () => {
mockGet.mockResolvedValueOnce({
data: expect.anything(),
});
mockGet.mockResolvedValueOnce(new Promise(resolve => resolve({ data: expect.anything() })));
await getKubernetesNamespace();

expect(mockGet).toBeCalledWith('/api/kubernetes/namespace');
expect(mockGet).toBeCalledWith('/api/kubernetes/namespace', undefined);
expect(mockPost).not.toBeCalled();
});

it('should return a list of namespaces', async () => {
mockGet.mockResolvedValueOnce({
data: [namespace],
});
mockGet.mockResolvedValueOnce(new Promise(resolve => resolve({ data: [namespace] })));

const res = await getKubernetesNamespace();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,28 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { delay } from '../helpers/delay';

const retryCount = 3;
const retryDelay = 500;

type AxiosFunc = (url: string, config?: AxiosRequestConfig) => Promise<any>;
type AxiosFuncWithData = (url: string, data?: any, config?: AxiosRequestConfig) => Promise<any>;

export const bearerTokenAuthorizationIsRequiredErrorMsg = 'Bearer Token Authorization is required';

export class AxiosWrapper {
protected readonly retryCount = 3;
protected readonly retryDelay = 500;
protected readonly axiosInstance: AxiosInstance;
protected readonly errorMessagesToRetry?: string;

constructor(axiosInstance: AxiosInstance, errorMessagesToRetry?: string) {
this.axiosInstance = axiosInstance;
this.errorMessagesToRetry = errorMessagesToRetry;
}

constructor(axiosInstance?: AxiosInstance) {
this.axiosInstance = axiosInstance || axios.create();
static createToRetryMissedBearerTokenError(): AxiosWrapper {
return new AxiosWrapper(axios.create(), bearerTokenAuthorizationIsRequiredErrorMsg);
}

static create(): AxiosWrapper {
return new AxiosWrapper();
static createToRetryAnyErrors(): AxiosWrapper {
return new AxiosWrapper(axios.create());
}

get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R> {
Expand Down Expand Up @@ -67,7 +74,7 @@ export class AxiosWrapper {
url: string,
config?: any,
): Promise<R> {
return this.doRetryFunc(() => axiosFunc(url, config), url, retryCount);
return this.doRetryFunc(() => axiosFunc(url, config), url, this.retryCount);
}

private retryAxiosFuncWithData<T = any, R = AxiosResponse<T>>(
Expand All @@ -76,7 +83,7 @@ export class AxiosWrapper {
data?: string,
config?: any,
): Promise<R> {
return this.doRetryFunc(() => axiosFunc(url, data, config), url, retryCount);
return this.doRetryFunc(() => axiosFunc(url, data, config), url, this.retryCount);
}

async doRetryFunc<T = any, R = AxiosResponse<T>>(
Expand All @@ -87,13 +94,16 @@ export class AxiosWrapper {
try {
return await fun();
} catch (err) {
if (!retry || !(err as Error)?.message?.includes('Bearer Token Authorization is required')) {
if (
!retry ||
(this.errorMessagesToRetry && !(err as Error)?.message?.includes(this.errorMessagesToRetry))
) {
throw err;
}

// Retry the request after a delay.
console.warn(`Retrying request ${url}... ${retry} left`);
await delay(retryDelay);
console.warn(`Retrying request to ${url} in ${this.retryDelay} ms, ${retry} left`);
await delay(this.retryDelay);

return await this.doRetryFunc(fun, url, --retry);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function createWorkspace(
devworkspace: devfileApi.DevWorkspace,
): Promise<{ devWorkspace: devfileApi.DevWorkspace; headers: Headers }> {
try {
const response = await AxiosWrapper.create().post(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().post(
`${dashboardBackendPrefix}/namespace/${devworkspace.metadata.namespace}/devworkspaces`,
{ devworkspace },
);
Expand All @@ -41,7 +41,7 @@ export async function listWorkspacesInNamespace(
defaultNamespace: string,
): Promise<IDevWorkspacesList> {
try {
const response = await AxiosWrapper.create().get(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().get(
`${dashboardBackendPrefix}/namespace/${defaultNamespace}/devworkspaces`,
);
return response.data;
Expand All @@ -55,7 +55,7 @@ export async function getWorkspaceByName(
workspaceName: string,
): Promise<devfileApi.DevWorkspace> {
try {
const response = await AxiosWrapper.create().get(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().get(
`${dashboardBackendPrefix}/namespace/${namespace}/devworkspaces/${workspaceName}`,
);
return response.data;
Expand All @@ -72,7 +72,7 @@ export async function patchWorkspace(
patch: api.IPatch[],
): Promise<{ devWorkspace: devfileApi.DevWorkspace; headers: Headers }> {
try {
const response = await AxiosWrapper.create().patch(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().patch(
`${dashboardBackendPrefix}/namespace/${namespace}/devworkspaces/${workspaceName}`,
patch,
);
Expand All @@ -86,7 +86,7 @@ export async function patchWorkspace(

export async function deleteWorkspace(namespace: string, workspaceName: string): Promise<void> {
try {
await AxiosWrapper.create().delete(
await AxiosWrapper.createToRetryMissedBearerTokenError().delete(
`${dashboardBackendPrefix}/namespace/${namespace}/devworkspaces/${workspaceName}`,
);
} catch (e) {
Expand All @@ -98,7 +98,7 @@ export async function deleteWorkspace(namespace: string, workspaceName: string):

export async function getDockerConfig(namespace: string): Promise<api.IDockerConfig> {
try {
const response = await AxiosWrapper.create().get(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().get(
`${dashboardBackendPrefix}/namespace/${namespace}/dockerconfig`,
);
return response.data;
Expand All @@ -112,7 +112,7 @@ export async function putDockerConfig(
dockerconfig: api.IDockerConfig,
): Promise<api.IDockerConfig> {
try {
const response = await AxiosWrapper.create().put(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().put(
`${dashboardBackendPrefix}/namespace/${namespace}/dockerconfig`,
dockerconfig,
);
Expand All @@ -124,7 +124,7 @@ export async function putDockerConfig(

export async function injectKubeConfig(namespace: string, devworkspaceId: string): Promise<void> {
try {
await AxiosWrapper.create().post(
await AxiosWrapper.createToRetryMissedBearerTokenError().post(
`${dashboardBackendPrefix}/namespace/${namespace}/devworkspaceId/${devworkspaceId}/kubeconfig`,
);
} catch (e) {
Expand All @@ -134,7 +134,7 @@ export async function injectKubeConfig(namespace: string, devworkspaceId: string

export async function podmanLogin(namespace: string, devworkspaceId: string): Promise<void> {
try {
await AxiosWrapper.create().post(
await AxiosWrapper.createToRetryMissedBearerTokenError().post(
`${dashboardBackendPrefix}/namespace/${namespace}/devworkspaceId/${devworkspaceId}/podmanlogin`,
);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export async function createTemplate(
): Promise<devfileApi.DevWorkspaceTemplate> {
const url = `${dashboardBackendPrefix}/namespace/${template.metadata.namespace}/devworkspacetemplates`;
try {
const response = await AxiosWrapper.create().post(url, { template });
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().post(url, {
template,
});
return response.data;
} catch (e) {
throw new Error(
Expand All @@ -32,7 +34,7 @@ export async function createTemplate(
export async function getTemplates(namespace: string): Promise<devfileApi.DevWorkspaceTemplate[]> {
const url = `${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates`;
try {
const response = await AxiosWrapper.create().get(url);
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().get(url);
return response.data;
} catch (e) {
throw new Error(
Expand All @@ -48,7 +50,7 @@ export async function patchTemplate(
): Promise<devfileApi.DevWorkspace> {
const url = `${dashboardBackendPrefix}/namespace/${namespace}/devworkspacetemplates/${templateName}`;
try {
const response = await AxiosWrapper.create().patch(url, patch);
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().patch(url, patch);
return response.data;
} catch (e) {
throw new Error(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { AxiosWrapper } from './axiosWrapper';

export async function fetchEvents(namespace: string): Promise<api.IEventList> {
try {
const response = await AxiosWrapper.create().get(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().get(
`${dashboardBackendPrefix}/namespace/${namespace}/events`,
);
return response.data;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { dashboardBackendPrefix } from './const';
import { AxiosWrapper } from './axiosWrapper';

export async function fetchGitConfig(namespace: string): Promise<api.IGitConfig> {
const response = await AxiosWrapper.create().get(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().get(
`${dashboardBackendPrefix}/namespace/${namespace}/gitconfig`,
);
return response.data;
Expand All @@ -25,7 +25,7 @@ export async function patchGitConfig(
namespace: string,
gitconfig: api.IGitConfig,
): Promise<api.IGitConfig> {
const response = await AxiosWrapper.create().patch(
const response = await AxiosWrapper.createToRetryMissedBearerTokenError().patch(
`${dashboardBackendPrefix}/namespace/${namespace}/gitconfig`,
gitconfig,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@

import axios from 'axios';
import { cheServerPrefix } from './const';
import { AxiosWrapper } from './axiosWrapper';

export async function getKubernetesNamespace(): Promise<che.KubernetesNamespace[]> {
const response = await axios.get(`${cheServerPrefix}/kubernetes/namespace`);
const response = await AxiosWrapper.createToRetryAnyErrors().get(
`${cheServerPrefix}/kubernetes/namespace`,
);

return response.data;
}
Expand Down
Loading

0 comments on commit 1966f74

Please sign in to comment.