diff --git a/src/client.tsx b/src/client.tsx
index 0ff20a0883..ffeba12e80 100644
--- a/src/client.tsx
+++ b/src/client.tsx
@@ -31,7 +31,8 @@ import "@fontsource/source-serif-pro/index.css";
import { i18nInstance } from "@ndla/ui";
import { getCookie, setCookie } from "@ndla/util";
import App from "./App";
-import ResponseContext from "./components/ResponseContext";
+import GQLErrorContext, { ErrorContextInfo } from "./components/GQLErrorContext";
+import ResponseContext, { ResponseInfo } from "./components/ResponseContext";
import { VersionHashProvider } from "./components/VersionHashContext";
import { STORED_LANGUAGE_COOKIE_KEY } from "./constants";
import { getLocaleInfoFromPath, initializeI18n, isValidLocale, supportedLanguages } from "./i18n";
@@ -45,7 +46,7 @@ declare global {
}
const {
- DATA: { config, serverPath, serverQuery, serverResponse },
+ DATA: { config, serverPath, serverQuery, serverResponse, serverErrorContext },
} = window;
initSentry(config);
@@ -179,17 +180,21 @@ const renderOrHydrate = (container: HTMLElement, children: ReactNode) => {
hydrateRoot(container, children);
}
};
+const responseContext = new ResponseInfo(serverResponse);
+const errorContext = new ErrorContextInfo(serverErrorContext);
renderOrHydrate(
document.getElementById("root")!,
-
-
-
-
-
+
+
+
+
+
+
+
,
diff --git a/src/components/GQLErrorContext.tsx b/src/components/GQLErrorContext.tsx
new file mode 100644
index 0000000000..c6ecb4c27a
--- /dev/null
+++ b/src/components/GQLErrorContext.tsx
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2024-present, NDLA.
+ *
+ * This source code is licensed under the GPLv3 license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import { createContext, useContext } from "react";
+import { ApolloError, DocumentNode, OperationVariables, TypedDocumentNode } from "@apollo/client";
+import handleError from "../util/handleError";
+
+export interface ServerErrorContext {
+ error: string | undefined;
+}
+
+export class ErrorContextInfo {
+ error: { [key: string]: ApolloError };
+ constructor(serverErrorContext?: ServerErrorContext) {
+ this.error = serverErrorContext?.error ? JSON.parse(serverErrorContext.error) : {};
+ }
+
+ getError(key: string): ApolloError | undefined {
+ return this.error[key];
+ }
+
+ setError(key: string, error: ApolloError) {
+ this.error[key] = error;
+ }
+
+ serialize(): ServerErrorContext {
+ return { error: JSON.stringify(this.error) };
+ }
+}
+
+const GQLErrorContext = createContext(new ErrorContextInfo());
+
+export const useApolloErrors = (
+ errors: ApolloError | undefined,
+ query: DocumentNode | TypedDocumentNode,
+): ApolloError | undefined => {
+ const operation = query.definitions.find((definition) => definition.kind === "OperationDefinition");
+ const queryKey = operation?.name?.value;
+ const errorContext = useContext(GQLErrorContext);
+ if (!queryKey) {
+ handleError(new Error("No operation name found when using useApolloErrors, seems like a bug..."));
+ return errors;
+ }
+
+ if (errors) {
+ errorContext.setError(queryKey, errors);
+ return errors;
+ }
+
+ return errorContext.getError(queryKey);
+};
+export default GQLErrorContext;
diff --git a/src/components/ResponseContext.tsx b/src/components/ResponseContext.tsx
index d807644ca3..39e4df967a 100644
--- a/src/components/ResponseContext.tsx
+++ b/src/components/ResponseContext.tsx
@@ -8,8 +8,21 @@
import { createContext } from "react";
-export interface ResponseInfo {
+export class ResponseInfo {
status?: number;
+
+ constructor(status?: number) {
+ this.status = status;
+ }
+
+ isAccessDeniedError(): boolean {
+ return this.status === 401 || this.status === 403;
+ }
+
+ isGoneError(): boolean {
+ return this.status === 410;
+ }
}
+
const ResponseContext = createContext(undefined);
export default ResponseContext;
diff --git a/src/containers/PlainArticlePage/PlainArticlePage.tsx b/src/containers/PlainArticlePage/PlainArticlePage.tsx
index cd6e52c795..8a94757b93 100644
--- a/src/containers/PlainArticlePage/PlainArticlePage.tsx
+++ b/src/containers/PlainArticlePage/PlainArticlePage.tsx
@@ -58,12 +58,9 @@ const PlainArticlePage = () => {
if (loading) {
return ;
}
- if (error?.graphQLErrors.some((err) => err.extensions.status === 410) && redirectContext) {
- redirectContext.status = 410;
- return ;
- }
-
- if (responseContext?.status === 410) {
+ const has410Error = error?.graphQLErrors.some((err) => err.extensions.status === 410);
+ if (has410Error || responseContext?.isGoneError()) {
+ if (redirectContext) redirectContext.status = 410;
return ;
}
diff --git a/src/containers/ResourcePage/ResourcePage.tsx b/src/containers/ResourcePage/ResourcePage.tsx
index 9f23b1ff4e..26780aab52 100644
--- a/src/containers/ResourcePage/ResourcePage.tsx
+++ b/src/containers/ResourcePage/ResourcePage.tsx
@@ -102,12 +102,12 @@ const ResourcePage = () => {
}
const accessDeniedErrors = findAccessDeniedErrors(error);
- if (accessDeniedErrors) {
+ if (accessDeniedErrors || responseContext?.isAccessDeniedError()) {
const nonRecoverableError = accessDeniedErrors.some(
(e) => !e.path?.includes("coreResources") && !e.path?.includes("supplementaryResources"),
);
- if (nonRecoverableError) {
+ if (nonRecoverableError || responseContext?.isAccessDeniedError()) {
return ;
}
}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 22783631af..fe7171512e 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -6,6 +6,7 @@
*
*/
import { NormalizedCacheObject } from "@apollo/client";
+import { ServerErrorContext } from "./components/GQLErrorContext";
import { ConfigType } from "./config";
import { LocaleValues } from "./constants";
@@ -30,6 +31,7 @@ export interface WindowData {
[key: string]: string | number | boolean | undefined | null;
};
serverResponse?: number;
+ serverErrorContext?: ServerErrorContext;
}
export interface NDLAWindow {
diff --git a/src/server/render/defaultRender.tsx b/src/server/render/defaultRender.tsx
index c18d772b8f..33dbc91525 100644
--- a/src/server/render/defaultRender.tsx
+++ b/src/server/render/defaultRender.tsx
@@ -15,6 +15,7 @@ import { i18nInstance } from "@ndla/ui";
import { getCookie } from "@ndla/util";
import { disableSSR } from "./renderHelpers";
import App from "../../App";
+import GQLErrorContext, { ErrorContextInfo, ServerErrorContext } from "../../components/GQLErrorContext";
import RedirectContext, { RedirectInfo } from "../../components/RedirectContext";
import ResponseContext, { ResponseInfo } from "../../components/ResponseContext";
import { VersionHashProvider } from "../../components/VersionHashContext";
@@ -69,7 +70,8 @@ export const defaultRender: RenderFunc = async (req) => {
const client = createApolloClient(locale, versionHash, req.path);
const i18n = initializeI18n(i18nInstance, locale);
const redirectContext: RedirectInfo = {};
- const responseContext: ResponseInfo = {};
+ const responseContext: ResponseInfo = new ResponseInfo();
+ const errorContext = new ErrorContextInfo();
// @ts-ignore
const helmetContext: FilledContext = {};
@@ -78,15 +80,17 @@ export const defaultRender: RenderFunc = async (req) => {
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
@@ -104,6 +108,8 @@ export const defaultRender: RenderFunc = async (req) => {
const apolloState = client.extract();
+ const serverErrorContext: ServerErrorContext = errorContext.serialize();
+
return {
status: redirectContext.status ?? OK,
data: {
@@ -111,6 +117,7 @@ export const defaultRender: RenderFunc = async (req) => {
htmlContent: html,
data: {
serverResponse: redirectContext.status ?? undefined,
+ serverErrorContext,
serverPath: req.path,
serverQuery: req.query,
apolloState,
diff --git a/src/util/runQueries.ts b/src/util/runQueries.ts
index 452cbbf8f5..6c14486de5 100644
--- a/src/util/runQueries.ts
+++ b/src/util/runQueries.ts
@@ -13,14 +13,20 @@ import {
TypedDocumentNode,
useQuery,
} from "@apollo/client";
+import { useApolloErrors } from "../components/GQLErrorContext";
export function useGraphQuery(
query: DocumentNode | TypedDocumentNode,
options?: QueryHookOptions,
): QueryResult {
- return useQuery(query, {
+ const result = useQuery(query, {
errorPolicy: "all",
ssr: true,
...options,
});
+
+ // Apollo client does not cache errors, we need some way to pass errors to the client if they occur on the server side
+ const error = useApolloErrors(result.error, query);
+
+ return { ...result, error };
}