Skip to content
Merged
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
19 changes: 18 additions & 1 deletion client/app/api/ErrorHandling.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AxiosResponse } from 'axios';
import { AxiosError, AxiosResponse } from 'axios';

import { AUTH_USER_MANAGER } from 'lib/components/wrappers/AuthProvider';
import {
Expand Down Expand Up @@ -37,3 +37,20 @@ export const redirectIfMatchesErrorIn = (response?: AxiosResponse): void => {
redirectToForbidden();
if (isComponentNotFoundResponse(response)) redirectToNotFound();
};

/**
* Redirects to the not-found page if `error` is a 404 response, and returns whether it
* did, so callers can skip their own error handling.
*
* The backend 404s when a resource doesn't exist under the parent resource in the URL,
* even if it exists under another parent, so this covers both a nonexistent ID and an
* ID belonging to someone else's course or assessment.
*
* This is opt-in rather than part of `redirectIfMatchesErrorIn` because some endpoints
* legitimately 404 as part of their normal flow, and expect to handle it themselves.
*/
export const redirectToNotFoundIfMissing = (error: unknown): boolean => {
const missing = (error as AxiosError)?.response?.status === 404;
if (missing) redirectToNotFound();
return missing;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isUnauthenticatedAssessmentData,
} from 'types/course/assessment/assessments';

import { redirectToNotFoundIfMissing } from 'api/ErrorHandling';
import LoadingIndicator from 'lib/components/core/LoadingIndicator';
import Preload from 'lib/components/wrappers/Preload';

Expand All @@ -19,8 +20,16 @@ const AssessmentShow = (): JSX.Element => {
const id = parseInt(params?.assessmentId ?? '', 10) || undefined;
if (!id) throw new Error(`AssessmentShow was loaded with ID: ${id}.`);

const fetchAssessmentWithId = (): Promise<FetchAssessmentData> =>
fetchAssessment(id);
const fetchAssessmentWithId = async (): Promise<FetchAssessmentData> => {
try {
return await fetchAssessment(id);
} catch (error) {
// An ID that matches no assessment in this course 404s from the backend. Show the
// not-found page rather than `Preload`'s generic fetching error.
redirectToNotFoundIfMissing(error);
throw error;
}
Comment thread
adi-herwana-nus marked this conversation as resolved.
};

return (
<Preload render={<LoadingIndicator />} while={fetchAssessmentWithId}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import GlobalAPI from 'api';
import CourseAPI from 'api/course';
import { redirectToNotFoundIfMissing } from 'api/ErrorHandling';
import { setNotification } from 'lib/actions';
import pollJob from 'lib/helpers/jobHelpers';

Expand Down Expand Up @@ -102,7 +103,12 @@ export function fetchSubmission(id, onGetMonitoringSessionId) {
}),
);
})
.catch(() => {
.catch((error) => {
// An ID that matches no submission in this assessment 404s from the backend. The
// page has nothing to render for it, so show the not-found page instead of an
// empty attempt page.
if (redirectToNotFoundIfMissing(error)) return;

dispatch({ type: actionTypes.FETCH_SUBMISSION_FAILURE });
dispatch(resetExistingAnswerFlags());
});
Expand Down
12 changes: 10 additions & 2 deletions client/app/bundles/course/assessment/submission/actions/logs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { LogInfo } from 'types/course/assessment/submission/logs';

import CourseAPI from 'api/course';
import { redirectToNotFoundIfMissing } from 'api/ErrorHandling';

const fetchLogs = async (): Promise<LogInfo> => {
const response = await CourseAPI.assessment.logs.index();
return response.data;
try {
const response = await CourseAPI.assessment.logs.index();
return response.data;
} catch (error) {
// As with the attempt page, an ID that matches no submission in this assessment 404s
// from the backend, and there are no logs to show for it.
redirectToNotFoundIfMissing(error);
throw error;
}
Comment thread
adi-herwana-nus marked this conversation as resolved.
};

export default fetchLogs;
8 changes: 7 additions & 1 deletion client/app/routers/course/assessments/submissions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RouteObject } from 'react-router-dom';
import { Navigate, RouteObject } from 'react-router-dom';
import { WithRequired } from 'types';

import { Translated } from 'lib/hooks/useTranslation';
Expand All @@ -25,6 +25,12 @@ const submissionsRouter: Translated<RouteObject> = (_) => ({
{
path: ':submissionId',
children: [
{
// A submission on its own has no page of its own to show, so send it to
// the attempt page instead of rendering an empty outlet.
index: true,
element: <Navigate replace to="edit" />,
},
{
path: 'edit',
lazy: async (): Promise<WithRequired<RouteObject, 'Component'>> => {
Expand Down