Skip to content

Commit

Permalink
Add GQLErrorContext that passes errors from server to client
Browse files Browse the repository at this point in the history
  • Loading branch information
jnatten committed Nov 14, 2024
1 parent 40a5904 commit 8c3b975
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 16 deletions.
16 changes: 10 additions & 6 deletions src/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import "@fontsource/source-serif-pro/index.css";
import { i18nInstance } from "@ndla/ui";
import { getCookie, setCookie } from "@ndla/util";
import App from "./App";
import GQLErrorContext, { ErrorContextInfo } from "./components/GQLErrorContext";
import ResponseContext, { ResponseInfo } from "./components/ResponseContext";
import { VersionHashProvider } from "./components/VersionHashContext";
import { STORED_LANGUAGE_COOKIE_KEY } from "./constants";
Expand All @@ -45,7 +46,7 @@ declare global {
}

const {
DATA: { config, serverPath, serverQuery, serverResponse },
DATA: { config, serverPath, serverQuery, serverResponse, serverErrorContext },
} = window;

initSentry(config);
Expand Down Expand Up @@ -180,17 +181,20 @@ const renderOrHydrate = (container: HTMLElement, children: ReactNode) => {
}
};
const responseContext = new ResponseInfo(serverResponse);
const errorContext = new ErrorContextInfo(serverErrorContext);

renderOrHydrate(
document.getElementById("root")!,
<HelmetProvider>
<I18nextProvider i18n={i18n}>
<ApolloProvider client={client}>
<ResponseContext.Provider value={responseContext}>
<VersionHashProvider value={versionHash}>
<LanguageWrapper basename={basename} />
</VersionHashProvider>
</ResponseContext.Provider>
<GQLErrorContext.Provider value={errorContext}>
<ResponseContext.Provider value={responseContext}>
<VersionHashProvider value={versionHash}>
<LanguageWrapper basename={basename} />
</VersionHashProvider>
</ResponseContext.Provider>
</GQLErrorContext.Provider>
</ApolloProvider>
</I18nextProvider>
</HelmetProvider>,
Expand Down
57 changes: 57 additions & 0 deletions src/components/GQLErrorContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ErrorContextInfo>(new ErrorContextInfo());

export const useApolloErrors = <TData extends any = any, TVariables extends OperationVariables = OperationVariables>(
errors: ApolloError | undefined,
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
): 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;
2 changes: 2 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
*/
import { NormalizedCacheObject } from "@apollo/client";
import { ServerErrorContext } from "./components/GQLErrorContext";
import { ConfigType } from "./config";
import { LocaleValues } from "./constants";

Expand All @@ -30,6 +31,7 @@ export interface WindowData {
[key: string]: string | number | boolean | undefined | null;
};
serverResponse?: number;
serverErrorContext?: ServerErrorContext;
}

export interface NDLAWindow {
Expand Down
25 changes: 16 additions & 9 deletions src/server/render/defaultRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { MOVED_PERMANENTLY, OK, TEMPORARY_REDIRECT } from "../../statusCodes";
import { UserAgentProvider } from "../../UserAgentContext";
import { createApolloClient } from "../../util/apiHelpers";
import { RenderFunc } from "../serverHelpers";
import GQLErrorContext, { ErrorContextInfo, ServerErrorContext } from "../../components/GQLErrorContext";

Check warning on line 29 in src/server/render/defaultRender.tsx

View workflow job for this annotation

GitHub Actions / Unit tests

`../../components/GQLErrorContext` import should occur before import of `../../components/RedirectContext`

Check warning on line 29 in src/server/render/defaultRender.tsx

View workflow job for this annotation

GitHub Actions / Unit tests

`../../components/GQLErrorContext` import should occur before import of `../../components/RedirectContext`

function getCookieLocaleOrFallback(resCookie: string, abbreviation: LocaleType) {
const cookieLocale = getCookie(STORED_LANGUAGE_COOKIE_KEY, resCookie) ?? "";
Expand Down Expand Up @@ -70,6 +71,7 @@ export const defaultRender: RenderFunc = async (req) => {
const i18n = initializeI18n(i18nInstance, locale);
const redirectContext: RedirectInfo = {};
const responseContext: ResponseInfo = new ResponseInfo();
const errorContext = new ErrorContextInfo();
// @ts-ignore
const helmetContext: FilledContext = {};

Expand All @@ -78,15 +80,17 @@ export const defaultRender: RenderFunc = async (req) => {
<HelmetProvider context={helmetContext}>
<I18nextProvider i18n={i18n}>
<ApolloProvider client={client}>
<ResponseContext.Provider value={responseContext}>
<VersionHashProvider value={versionHash}>
<UserAgentProvider value={userAgentSelectors}>
<StaticRouter basename={basename} location={req.url}>
<App key={locale} />
</StaticRouter>
</UserAgentProvider>
</VersionHashProvider>
</ResponseContext.Provider>
<GQLErrorContext.Provider value={errorContext}>
<ResponseContext.Provider value={responseContext}>
<VersionHashProvider value={versionHash}>
<UserAgentProvider value={userAgentSelectors}>
<StaticRouter basename={basename} location={req.url}>
<App key={locale} />
</StaticRouter>
</UserAgentProvider>
</VersionHashProvider>
</ResponseContext.Provider>
</GQLErrorContext.Provider>
</ApolloProvider>
</I18nextProvider>
</HelmetProvider>
Expand All @@ -104,13 +108,16 @@ export const defaultRender: RenderFunc = async (req) => {

const apolloState = client.extract();

const serverErrorContext: ServerErrorContext = errorContext.serialize();

return {
status: redirectContext.status ?? OK,
data: {
helmetContext,
htmlContent: html,
data: {
serverResponse: redirectContext.status ?? undefined,
serverErrorContext,
serverPath: req.path,
serverQuery: req.query,
apolloState,
Expand Down
8 changes: 7 additions & 1 deletion src/util/runQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,20 @@ import {
TypedDocumentNode,
useQuery,
} from "@apollo/client";
import { useApolloErrors } from "../components/GQLErrorContext";

export function useGraphQuery<TData extends any = any, TVariables extends OperationVariables = OperationVariables>(
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
options?: QueryHookOptions<TData, TVariables>,
): QueryResult<TData, TVariables> {
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 };
}

0 comments on commit 8c3b975

Please sign in to comment.