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 }; }