diff --git a/client/src/components/layouts/default.tsx b/client/src/components/layouts/default.tsx
index a135a4f5bb9..b9da69a81fc 100644
--- a/client/src/components/layouts/default.tsx
+++ b/client/src/components/layouts/default.tsx
@@ -181,7 +181,7 @@ function DefaultLayout({
const isJapanese = clientLocale === 'japanese';
- if (fetchState.pending) {
+ if (!fetchState.complete) {
return ;
} else {
return (
diff --git a/client/src/redux/action-types.js b/client/src/redux/action-types.js
index 0eb33759224..d242e461e26 100644
--- a/client/src/redux/action-types.js
+++ b/client/src/redux/action-types.js
@@ -46,7 +46,8 @@ export const actionTypes = createTypes(
...createAsyncTypes('showCert'),
...createAsyncTypes('reportUser'),
...createAsyncTypes('deleteUserToken'),
- ...createAsyncTypes('saveChallenge')
+ ...createAsyncTypes('saveChallenge'),
+ 'fetchUserTimeout'
],
ns
);
diff --git a/client/src/redux/actions.ts b/client/src/redux/actions.ts
index ebc23b054e9..a6d2ed2f9ef 100644
--- a/client/src/redux/actions.ts
+++ b/client/src/redux/actions.ts
@@ -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);
diff --git a/client/src/redux/fetch-user-saga.js b/client/src/redux/fetch-user-saga.js
index 6a2d8835be6..d093dda4d66 100644
--- a/client/src/redux/fetch-user-saga.js
+++ b/client/src/redux/fetch-user-saga.js
@@ -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);
}
}
diff --git a/client/src/redux/index.js b/client/src/redux/index.js
index fc8b939f6d8..2335cd15dfe 100644
--- a/client/src/redux/index.js
+++ b/client/src/redux/index.js
@@ -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
}
diff --git a/client/src/utils/ajax.ts b/client/src/utils/ajax.ts
index 06023558227..bb045e62059 100644
--- a/client/src/utils/ajax.ts
+++ b/client/src/utils/ajax.ts
@@ -37,10 +37,14 @@ export interface ResponseWithData {
// 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(path: string): Promise> {
+async function get(
+ path: string,
+ signal?: AbortSignal
+): Promise> {
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(
return files.map(({ key, ...rest }) => ({ ...rest, fileKey: key }));
}
-export function getSessionUser(): Promise> {
+export function getSessionUser(
+ signal?: AbortSignal
+): Promise> {
const responseWithData: Promise> = 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 }) => {