Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error handling for OG #561

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@
QueryClientProvider,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AxiosError } from 'axios';
import React from 'react';
import { connect, Provider } from 'react-redux';
import {
clearFailedAuthRequestsQueue,
retryFailedAuthRequests,
} from './api/api';
import './App.css';
import { MicroFrontendId } from './app.types';
import handleOG_APIError from './handleOG_APIError';
import OGThemeProvider from './ogThemeProvider.component';
import Preloader from './preloader/preloader.component';
import retryOG_APIErrors from './retryOG_APIErrors';
import SettingsMenuItems from './settingsMenuItems.component';
import { requestPluginRerender } from './state/scigateway.actions';
import {
broadcastSignOut,
requestPluginRerender,
tokenRefreshed,
} from './state/scigateway.actions';
import { configureApp } from './state/slices/configSlice';
import { RootState, store } from './state/store';
import ViewTabs from './views/viewTabs.component';
Expand All @@ -23,12 +34,15 @@
queries: {
refetchOnWindowFocus: true,
staleTime: 300000,
retry: (failureCount, error) => {
return retryOG_APIErrors(failureCount, error as AxiosError);
},

Check warning on line 39 in src/App.tsx

View check run for this annotation

Codecov / codecov/patch

src/App.tsx#L38-L39

Added lines #L38 - L39 were not covered by tests
},
},
// TODO: implement proper error handling

queryCache: new QueryCache({
onError: (error) => {
console.log('Got error ' + error.message);
handleOG_APIError(error as AxiosError);

Check warning on line 45 in src/App.tsx

View check run for this annotation

Codecov / codecov/patch

src/App.tsx#L45

Added line #L45 was not covered by tests
},
}),
});
Expand Down Expand Up @@ -58,7 +72,8 @@
const action = (e as CustomEvent).detail;
if (requestPluginRerender.match(action)) {
forceUpdate();
}
} else if (tokenRefreshed.match(action)) retryFailedAuthRequests();
else if (broadcastSignOut.match(action)) clearFailedAuthRequestsQueue();

Check warning on line 76 in src/App.tsx

View check run for this annotation

Codecov / codecov/patch

src/App.tsx#L75-L76

Added lines #L75 - L76 were not covered by tests
}

React.useEffect(() => {
Expand Down
88 changes: 88 additions & 0 deletions src/api/api.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import axios from 'axios';
import { MicroFrontendId, type APIError } from '../app.types';
import { readSciGatewayToken } from '../parseTokens';
import { settings } from '../settings';
import { InvalidateTokenType } from '../state/scigateway.actions';

// These are for ensuring refresh request is only sent once when multiple requests
// are failing due to 403's at the same time
let isFetchingAccessToken = false;
let failedAuthRequestQueue: ((shouldReject?: boolean) => void)[] = [];

/* This should be called when SciGateway successfully refreshes the access token - it retries
all requests that failed due to an invalid token */
export const retryFailedAuthRequests = () => {
isFetchingAccessToken = false;
failedAuthRequestQueue.forEach((callback) => callback());
failedAuthRequestQueue = [];
};

Check warning on line 18 in src/api/api.tsx

View check run for this annotation

Codecov / codecov/patch

src/api/api.tsx#L15-L18

Added lines #L15 - L18 were not covered by tests

/* This should be called when SciGateway logs out as would occur if a token refresh fails
due to the refresh token being out of date - it rejects all active request promises that
were awaiting a token refresh using the original error that occurred on the first attempt */
export const clearFailedAuthRequestsQueue = () => {
isFetchingAccessToken = false;
failedAuthRequestQueue.forEach((callback) => callback(true));
failedAuthRequestQueue = [];
};

Check warning on line 27 in src/api/api.tsx

View check run for this annotation

Codecov / codecov/patch

src/api/api.tsx#L24-L27

Added lines #L24 - L27 were not covered by tests

export const ogApi = axios.create();

ogApi.interceptors.request.use(async (config) => {
const settingsData = await settings;
config.baseURL = settingsData ? settingsData.apiUrl : '';
config.headers['Authorization'] = `Bearer ${readSciGatewayToken()}`;
return config;
});

ogApi.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;

const errorDetail = (error.response.data as APIError)?.detail;

const errorMessage =
typeof errorDetail === 'string'
? errorDetail.toLocaleLowerCase()
: error.message;

// Check if the token is invalid and needs refreshing
// only allow a request to be retried once. Don't retry if not logged
// in, it should not have been accessible
if (
error.response?.status === 403 &&
errorMessage.includes('invalid token') &&
!originalRequest._retried &&
localStorage.getItem('scigateway:token')

Check warning on line 57 in src/api/api.tsx

View check run for this annotation

Codecov / codecov/patch

src/api/api.tsx#L57

Added line #L57 was not covered by tests
) {
originalRequest._retried = true;

Check warning on line 59 in src/api/api.tsx

View check run for this annotation

Codecov / codecov/patch

src/api/api.tsx#L59

Added line #L59 was not covered by tests

// Prevent other requests from also attempting to refresh while waiting for
// SciGateway to refresh the token
if (!isFetchingAccessToken) {
isFetchingAccessToken = true;

Check warning on line 64 in src/api/api.tsx

View check run for this annotation

Codecov / codecov/patch

src/api/api.tsx#L63-L64

Added lines #L63 - L64 were not covered by tests

// Request SciGateway to refresh the token
document.dispatchEvent(
new CustomEvent(MicroFrontendId, {
detail: {
type: InvalidateTokenType,
},
})
);
}

Check warning on line 74 in src/api/api.tsx

View check run for this annotation

Codecov / codecov/patch

src/api/api.tsx#L67-L74

Added lines #L67 - L74 were not covered by tests

// Add request to queue to be resolved only once SciGateway has successfully
// refreshed the token
return new Promise((resolve, reject) => {
failedAuthRequestQueue.push((shouldReject?: boolean) => {
if (shouldReject) reject(error);
else resolve(ogApi(originalRequest));
});
});
}

Check warning on line 84 in src/api/api.tsx

View check run for this annotation

Codecov / codecov/patch

src/api/api.tsx#L78-L84

Added lines #L78 - L84 were not covered by tests
// Any other error
else return Promise.reject(error);
}
);
12 changes: 0 additions & 12 deletions src/api/channels.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,6 @@ describe('channels api functions', () => {

expect(result.current.data).toEqual([]);
});

it.todo(
'sends axios request to fetch channels and throws an appropriate error on failure'
);
});

describe('getScalarChannels', () => {
Expand Down Expand Up @@ -310,10 +306,6 @@ describe('channels api functions', () => {

expect(result.current.data).toEqual([]);
});

it.todo(
'sends axios request to fetch records and throws an appropriate error on failure'
);
});

describe('useChannelSummary', () => {
Expand Down Expand Up @@ -363,10 +355,6 @@ describe('channels api functions', () => {
expect(result.current.isPending).toBeTruthy();
expect(requestSent).toBe(false);
});

it.todo(
'sends axios request to fetch records and throws an appropriate error on failure'
);
});

describe('useScalarChannels', () => {
Expand Down
57 changes: 18 additions & 39 deletions src/api/channels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
UseQueryResult,
} from '@tanstack/react-query';
import { ColumnDef, createColumnHelper } from '@tanstack/react-table';
import axios, { AxiosError } from 'axios';
import { AxiosError } from 'axios';
import React from 'react';
import {
FullChannelMetadata,
Expand All @@ -16,16 +16,15 @@ import {
timeChannelName,
ValidateFunctionState,
} from '../app.types';
import { readSciGatewayToken } from '../parseTokens';
import { useAppDispatch, useAppSelector } from '../state/hooks';
import { selectUrls } from '../state/slices/configSlice';
import { selectAppliedFunctions } from '../state/slices/functionsSlice';
import { openImageWindow, openTraceWindow } from '../state/slices/windowSlice';
import { AppDispatch } from '../state/store';
import {
roundNumber,
TraceOrImageThumbnail,
} from '../table/cellRenderers/cellContentRenderers';
import { ogApi } from './api';
import { convertExpressionsToStrings } from './functions';

interface ChannelsEndpoint {
Expand Down Expand Up @@ -62,27 +61,17 @@ export const staticChannels: { [systemName: string]: FullChannelMetadata } = {
},
};

const fetchChannels = (apiUrl: string): Promise<FullChannelMetadata[]> => {
return axios
.get<ChannelsEndpoint>(`${apiUrl}/channels`, {
headers: {
Authorization: `Bearer ${readSciGatewayToken()}`,
},
const fetchChannels = async (): Promise<FullChannelMetadata[]> => {
const response = await ogApi.get<ChannelsEndpoint>(`/channels`);
const { channels } = response.data;
if (!channels || Object.keys(channels).length === 0) return [];
const convertedChannels: FullChannelMetadata[] = Object.entries(channels).map(
([systemName, channel]) => ({
systemName,
...channel,
})
.then((response) => {
const { channels } = response.data;

if (!channels || Object.keys(channels).length === 0) return [];

const convertedChannels: FullChannelMetadata[] = Object.entries(
channels
).map(([systemName, channel]) => ({
systemName,
...channel,
}));

return [...Object.values(staticChannels), ...convertedChannels];
});
);
return [...Object.values(staticChannels), ...convertedChannels];
};

export interface ChannelSummary {
Expand All @@ -91,19 +80,12 @@ export interface ChannelSummary {
recent_sample: { [timestamp: string]: string | number }[];
}

const fetchChannelSummary = (
apiUrl: string,
const fetchChannelSummary = async (
channel: string
): Promise<ChannelSummary> => {
return axios
.get(`${apiUrl}/channels/summary/${channel}`, {
headers: {
Authorization: `Bearer ${readSciGatewayToken()}`,
},
})
.then((response) => {
return response.data;
});
return ogApi
.get(`/channels/summary/${channel}`)
.then((response) => response.data);
};

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
Expand All @@ -113,12 +95,10 @@ export const useChannels = <T extends unknown = FullChannelMetadata[]>(
'queryKey'
>
): UseQueryResult<T, AxiosError> => {
const { apiUrl } = useAppSelector(selectUrls);

return useQuery({
queryKey: ['channels'],
queryFn: () => {
return fetchChannels(apiUrl);
return fetchChannels();
},

...(options ?? {}),
Expand All @@ -128,7 +108,6 @@ export const useChannels = <T extends unknown = FullChannelMetadata[]>(
export const useChannelSummary = (
channel: string | undefined
): UseQueryResult<ChannelSummary, AxiosError> => {
const { apiUrl } = useAppSelector(selectUrls);
const dataChannel =
typeof channel !== 'undefined' && !(channel in staticChannels)
? channel
Expand All @@ -138,7 +117,7 @@ export const useChannelSummary = (
queryKey: ['channelSummary', dataChannel],

queryFn: () => {
return fetchChannelSummary(apiUrl, dataChannel);
return fetchChannelSummary(dataChannel);
},

enabled: dataChannel.length !== 0,
Expand Down
4 changes: 0 additions & 4 deletions src/api/experiment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,5 @@ describe('channels api functions', () => {

expect(result.current.data).toEqual(expected);
});

it.todo(
'sends axios request to fetch records and throws an appropriate error on failure'
);
});
});
23 changes: 5 additions & 18 deletions src/api/experiment.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,18 @@
import { useQuery, UseQueryResult } from '@tanstack/react-query';
import axios, { AxiosError } from 'axios';
import { AxiosError } from 'axios';
import { ExperimentParams } from '../app.types';
import { readSciGatewayToken } from '../parseTokens';
import { useAppSelector } from '../state/hooks';
import { selectUrls } from '../state/slices/configSlice';
import { ogApi } from './api';

const fetchExperiment = (apiUrl: string): Promise<ExperimentParams[]> => {
return axios
.get(`${apiUrl}/experiments`, {
headers: {
Authorization: `Bearer ${readSciGatewayToken()}`,
},
})
.then((response) => {
return response.data;
});
const fetchExperiment = async (): Promise<ExperimentParams[]> => {
return ogApi.get(`/experiments`).then((response) => response.data);
};

export const useExperiment = (): UseQueryResult<
ExperimentParams[],
AxiosError
> => {
const { apiUrl } = useAppSelector(selectUrls);

return useQuery({
queryKey: ['experiments'],

queryFn: () => fetchExperiment(apiUrl),
queryFn: () => fetchExperiment(),
});
};
Loading
Loading