mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-06-13 21:02:08 +08:00
fix(client): gracefully handle errors while fetching user (#61623)
Co-authored-by: Shaun Hamilton <shauhami020@gmail.com>
This commit is contained in:
parent
69ebb7e37a
commit
7fdaa034c8
@ -181,7 +181,7 @@ function DefaultLayout({
|
||||
|
||||
const isJapanese = clientLocale === 'japanese';
|
||||
|
||||
if (fetchState.pending) {
|
||||
if (!fetchState.complete) {
|
||||
return <Loader fullScreen={true} messageDelay={5000} />;
|
||||
} else {
|
||||
return (
|
||||
|
||||
@ -46,7 +46,8 @@ export const actionTypes = createTypes(
|
||||
...createAsyncTypes('showCert'),
|
||||
...createAsyncTypes('reportUser'),
|
||||
...createAsyncTypes('deleteUserToken'),
|
||||
...createAsyncTypes('saveChallenge')
|
||||
...createAsyncTypes('saveChallenge'),
|
||||
'fetchUserTimeout'
|
||||
],
|
||||
ns
|
||||
);
|
||||
|
||||
@ -50,6 +50,7 @@ export const acceptTermsError = createAction(actionTypes.acceptTermsError);
|
||||
|
||||
export const fetchUser = createAction(actionTypes.fetchUser);
|
||||
export const fetchUserComplete = createAction(actionTypes.fetchUserComplete);
|
||||
export const fetchUserTimeout = createAction(actionTypes.fetchUserTimeout);
|
||||
export const fetchUserError = createAction(actionTypes.fetchUserError);
|
||||
|
||||
export const toggleTheme = createAction(actionTypes.toggleTheme);
|
||||
|
||||
@ -1,20 +1,41 @@
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
import { call, cancel, delay, fork, put, takeEvery } from 'redux-saga/effects';
|
||||
import { getSessionUser, getUserProfile } from '../utils/ajax';
|
||||
import {
|
||||
fetchProfileForUserComplete,
|
||||
fetchProfileForUserError,
|
||||
fetchUserComplete,
|
||||
fetchUserError
|
||||
fetchUserError,
|
||||
fetchUserTimeout
|
||||
} from './actions';
|
||||
|
||||
function* fetchSessionUser() {
|
||||
try {
|
||||
const { data: user } = yield call(getSessionUser);
|
||||
const timeoutTask = yield fork(function* () {
|
||||
// The server should not take anywhere near 2 seconds to respond. If it
|
||||
// does, we assume the request has failed and dispatch a timeout action to
|
||||
// dismiss the loading state.
|
||||
yield delay(2000);
|
||||
yield put(fetchUserTimeout());
|
||||
});
|
||||
|
||||
try {
|
||||
// This is on a longer timeout to make sure that users with slow connections
|
||||
// do, eventually, get signed in.
|
||||
const res = yield call(getSessionUser, AbortSignal.timeout(10000));
|
||||
|
||||
const isSignedOut = res.response.status === 401;
|
||||
if (!res.response.ok && !isSignedOut) {
|
||||
throw new Error(
|
||||
`HTTP Error: ${res.response.status} ${res.response.statusText}`
|
||||
);
|
||||
}
|
||||
|
||||
const { data: user } = res;
|
||||
yield put(fetchUserComplete({ user }));
|
||||
} catch (e) {
|
||||
console.log('failed to fetch user', e);
|
||||
yield put(fetchUserError(e));
|
||||
} finally {
|
||||
yield cancel(timeoutTask);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -218,11 +218,22 @@ export const reducer = handleActions(
|
||||
error: null
|
||||
}
|
||||
}),
|
||||
[actionTypes.fetchUserTimeout]: state => ({
|
||||
...state,
|
||||
userFetchState: {
|
||||
// Pending because the fetch may still complete. This allows the UI to
|
||||
// render what it can while waiting for the fetch to complete.
|
||||
pending: true,
|
||||
complete: true,
|
||||
errored: false,
|
||||
error: null
|
||||
}
|
||||
}),
|
||||
[actionTypes.fetchUserError]: (state, { payload }) => ({
|
||||
...state,
|
||||
userFetchState: {
|
||||
pending: false,
|
||||
complete: false,
|
||||
complete: true,
|
||||
errored: true,
|
||||
error: payload
|
||||
}
|
||||
|
||||
@ -37,10 +37,14 @@ export interface ResponseWithData<T> {
|
||||
|
||||
// TODO: Might want to handle flash messages as close to the request as possible
|
||||
// to make use of the Response object (message, status, etc)
|
||||
async function get<T>(path: string): Promise<ResponseWithData<T>> {
|
||||
async function get<T>(
|
||||
path: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<ResponseWithData<T>> {
|
||||
const response = await fetch(`${base}${path}`, {
|
||||
...defaultOptions,
|
||||
headers: { 'CSRF-Token': getCSRFToken() }
|
||||
headers: { 'CSRF-Token': getCSRFToken() },
|
||||
signal
|
||||
});
|
||||
|
||||
return combineDataWithResponse(response);
|
||||
@ -144,9 +148,12 @@ function mapKeyToFileKey<K>(
|
||||
return files.map(({ key, ...rest }) => ({ ...rest, fileKey: key }));
|
||||
}
|
||||
|
||||
export function getSessionUser(): Promise<ResponseWithData<User | null>> {
|
||||
export function getSessionUser(
|
||||
signal?: AbortSignal
|
||||
): Promise<ResponseWithData<User | null>> {
|
||||
const responseWithData: Promise<ResponseWithData<ApiUserResponse>> = get(
|
||||
'/user/get-session-user'
|
||||
'/user/get-session-user',
|
||||
signal
|
||||
);
|
||||
// TODO: Once DB is migrated, no longer need to parse `files` -> `challengeFiles` etc.
|
||||
return responseWithData.then(({ response, data }) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user