diff --git a/docs/docs/router.md b/docs/docs/router.md index 658a4b839432..970175da24e1 100644 --- a/docs/docs/router.md +++ b/docs/docs/router.md @@ -600,9 +600,9 @@ Or if the variable passed as a prop to a component can't be found: ![fatal_error_message_query](/img/router/fatal_error_message_query.png) -And if the page has a Cell, you'll see the Cell's request and response which may have contributed to the error: +And if the page has a Cell, you'll see the Cell's request which may have contributed to the error - but will depend on how your Suspense boundary is setup: -![fatal_error_message_request](/img/router/fatal_error_request.png) +![cell_error_request](/img/router/cell_req_error.png) ### In Production @@ -664,7 +664,7 @@ Note that if you're copy-pasting this example, it uses [Tailwind CSS](https://ta :::note Can I customize the development one? -As it's part of the RedwoodJS framework, you can't. But if there's a feature you want to add, let us know on the [forums](https://community.redwoodjs.com/). +As it's part of the RedwoodJS framework, you can't _change_ the dev fatal error page - but you can always build your own that takes the same props. If there's a feature you want to add to the built-in version, let us know on the [forums](https://community.redwoodjs.com/). ::: diff --git a/docs/static/img/router/cell_req_error.png b/docs/static/img/router/cell_req_error.png new file mode 100644 index 000000000000..2c31a2526334 Binary files /dev/null and b/docs/static/img/router/cell_req_error.png differ diff --git a/packages/web/src/apollo/links.tsx b/packages/web/src/apollo/links.tsx index 04721652d896..0316ba36c3bb 100644 --- a/packages/web/src/apollo/links.tsx +++ b/packages/web/src/apollo/links.tsx @@ -1,5 +1,5 @@ -import type { HttpOptions } from '@apollo/client' -import { ApolloLink, HttpLink } from '@apollo/client' +import type { HttpOptions, Operation } from '@apollo/client' +import { ApolloLink, HttpLink, Observable } from '@apollo/client' import { setContext } from '@apollo/client/link/context' import { print } from 'graphql/language/printer' @@ -8,28 +8,44 @@ export function createHttpLink( httpLinkConfig: HttpOptions | undefined ) { return new HttpLink({ - // @MARK: we have to construct the absoltue url for SSR uri, ...httpLinkConfig, // you can disable result caching here if you want to - // (this does not work if you are rendering your page with `export const dynamic = "force-static"`) + // @TODO: this is probably NextJS specific. Revisit once we have our own apollo package fetchOptions: { cache: 'no-store' }, }) } -export function createUpdateDataLink(data: any) { - return new ApolloLink((operation, forward) => { - const { operationName, query, variables } = operation - data.mostRecentRequest = {} - data.mostRecentRequest.operationName = operationName - data.mostRecentRequest.operationKind = query?.kind.toString() - data.mostRecentRequest.variables = variables - data.mostRecentRequest.query = query && print(operation.query) +function enhanceError(operation: Operation, error: any) { + const { operationName, query, variables } = operation - return forward(operation).map((result) => { - data.mostRecentResponse = result + error.__RedwoodEnhancedError = { + operationName, + operationKind: query?.kind.toString(), + variables, + query: query && print(query), + } - return result + return error +} + +export function createUpdateDataLink() { + return new ApolloLink((operation, forward) => { + return new Observable((observer) => { + forward(operation).subscribe({ + next(result) { + if (result.errors) { + result.errors.forEach((error) => { + enhanceError(operation, error) + }) + } + observer.next(result) + }, + error(error: any) { + observer.error(enhanceError(operation, error)) + }, + complete: observer.complete.bind(observer), + }) }) }) } @@ -96,7 +112,7 @@ export function createFinalLink({ export type RedwoodApolloLinkName = | 'withToken' | 'authMiddleware' - | 'updateDataApolloLink' + | 'enhanceErrorLink' | 'httpLink' export type RedwoodApolloLink< @@ -110,7 +126,7 @@ export type RedwoodApolloLink< export type RedwoodApolloLinks = [ RedwoodApolloLink<'withToken'>, RedwoodApolloLink<'authMiddleware'>, - RedwoodApolloLink<'updateDataApolloLink'>, + RedwoodApolloLink<'enhanceErrorLink'>, RedwoodApolloLink<'httpLink', HttpLink> ] diff --git a/packages/web/src/apollo/suspense.tsx b/packages/web/src/apollo/suspense.tsx index 01011f39f26e..312d7f87f5a8 100644 --- a/packages/web/src/apollo/suspense.tsx +++ b/packages/web/src/apollo/suspense.tsx @@ -10,10 +10,10 @@ import type { ApolloCache, ApolloClientOptions, + ApolloLink, HttpOptions, InMemoryCacheConfig, setLogVerbosity, - ApolloLink, } from '@apollo/client' import { setLogVerbosity as apolloSetLogVerbosity, @@ -24,10 +24,10 @@ import { ApolloNextAppProvider, NextSSRApolloClient, NextSSRInMemoryCache, - useSuspenseQuery, useBackgroundQuery, - useReadQuery, useQuery, + useReadQuery, + useSuspenseQuery, } from '@apollo/experimental-nextjs-app-support/ssr' import type { UseAuth } from '@redwoodjs/auth' @@ -127,13 +127,6 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ // See https://www.apollographql.com/docs/react/api/link/introduction. const { getToken, type: authProviderType } = useAuth() - // `updateDataApolloLink` keeps track of the most recent req/res data so they can be passed to - // any errors passed up to an error boundary. - const data = { - mostRecentRequest: undefined, - mostRecentResponse: undefined, - } as any - const { headers, uri } = useFetchConfig() const getGraphqlUrl = () => { @@ -157,17 +150,11 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ name: 'authMiddleware', link: createAuthApolloLink(authProviderType, headers), }, - // @TODO: do we need this in prod? I think it's only for dev errors - { name: 'updateDataApolloLink', link: createUpdateDataLink(data) }, + // @REVIEW: Should we take this out for prod? + { name: 'enhanceErrorLink', link: createUpdateDataLink() }, { name: 'httpLink', link: createHttpLink(getGraphqlUrl(), httpLinkConfig) }, ] - const extendErrorAndRethrow = (error: any, _errorInfo: React.ErrorInfo) => { - error['mostRecentRequest'] = data.mostRecentRequest - error['mostRecentResponse'] = data.mostRecentResponse - throw error - } - function makeClient() { // @MARK use special Apollo client return new NextSSRApolloClient({ @@ -181,30 +168,11 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{ return ( - {children} + {children} ) } -type ComponentDidCatch = React.ComponentLifecycle['componentDidCatch'] - -interface ErrorBoundaryProps { - error?: unknown - onError: NonNullable - children: React.ReactNode -} - -class ErrorBoundary extends React.Component { - componentDidCatch(...args: Parameters>) { - this.setState({}) - this.props.onError(...args) - } - - render() { - return this.props.children - } -} - export const RedwoodApolloProvider: React.FunctionComponent<{ graphQLClientConfig?: GraphQLClientConfigProp useAuth?: UseAuth diff --git a/packages/web/src/components/DevFatalErrorPage.tsx b/packages/web/src/components/DevFatalErrorPage.tsx index c382c115e78a..1f05224934a6 100644 --- a/packages/web/src/components/DevFatalErrorPage.tsx +++ b/packages/web/src/components/DevFatalErrorPage.tsx @@ -9,6 +9,7 @@ if (typeof window !== 'undefined') { import { useState } from 'react' +import type { GraphQLError } from 'graphql' import StackTracey from 'stacktracey' // RWJS_SRC_ROOT is defined and defaulted in webpack+vite to the base path @@ -27,14 +28,21 @@ if (/^[A-Z]:\\/.test(srcRoot)) { appRoot = srcRoot.substring(1) } +type RequestDetails = { + query: string + operationName: string + operationKind: string + variables: any +} + +interface EnhancedGqlError extends GraphQLError { + __RedwoodEnhancedError: RequestDetails +} + // Allow APIs client to attach response/request type ErrorWithRequestMeta = Error & { - mostRecentRequest?: { - query: string - operationName: string - operationKind: string - variables: any - } + mostRecentRequest?: RequestDetails + graphQLErrors: EnhancedGqlError[] mostRecentResponse?: any } @@ -90,9 +98,7 @@ export const DevFatalErrorPage = (props: { error?: ErrorWithRequestMeta }) => { ))} - {props.error.mostRecentRequest ? ( - - ) : null} + ) @@ -226,20 +232,28 @@ function ResponseRequest(props: { error: ErrorWithRequestMeta }) { const [openQuery, setOpenQuery] = useState(false) const [openResponse, setOpenResponse] = useState(false) + if (!props.error) { + return null + } + + const mostRecentRequest = + props.error.mostRecentRequest || + props.error.graphQLErrors?.find((gqlErr) => gqlErr.__RedwoodEnhancedError) + ?.__RedwoodEnhancedError + + // Does not exist with Suspense Cells + const mostRecentResponse = props.error.mostRecentResponse + return (
- {props.error.mostRecentRequest ? ( + {mostRecentRequest ? (
-

Request: {props.error.mostRecentRequest.operationName}

+

Request: {mostRecentRequest.operationName}

Variables:
-                {JSON.stringify(
-                  props.error.mostRecentRequest.variables,
-                  null,
-                  '  '
-                )}
+                {JSON.stringify(mostRecentRequest.variables, null, '  ')}
               
@@ -250,13 +264,13 @@ function ResponseRequest(props: { error: ErrorWithRequestMeta }) { onClick={() => setOpenQuery(!openQuery)} className={openQuery ? 'open' : 'preview'} > - {props.error.mostRecentRequest.query} + {mostRecentRequest.query}
) : null} - {props.error.mostRecentRequest ? ( + {mostRecentResponse ? (

Response

@@ -266,7 +280,7 @@ function ResponseRequest(props: { error: ErrorWithRequestMeta }) { onClick={() => setOpenResponse(!openResponse)} className={openResponse ? 'open' : 'preview'} > - {JSON.stringify(props.error.mostRecentResponse, null, ' ')} + {JSON.stringify(mostRecentResponse, null, ' ')}