Skip to content

Commit

Permalink
fix(apollo): Enhance error differently for Suspense Cells (#9640)
Browse files Browse the repository at this point in the history
Co-authored-by: Tobbe Lundberg <tobbe@tlundberg.com>
  • Loading branch information
dac09 and Tobbe authored Dec 27, 2023
1 parent 3d1179a commit 7870ce4
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 77 deletions.
6 changes: 3 additions & 3 deletions docs/docs/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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/).
:::
Expand Down
Binary file added docs/static/img/router/cell_req_error.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 33 additions & 17 deletions packages/web/src/apollo/links.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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),
})
})
})
}
Expand Down Expand Up @@ -96,7 +112,7 @@ export function createFinalLink({
export type RedwoodApolloLinkName =
| 'withToken'
| 'authMiddleware'
| 'updateDataApolloLink'
| 'enhanceErrorLink'
| 'httpLink'

export type RedwoodApolloLink<
Expand All @@ -110,7 +126,7 @@ export type RedwoodApolloLink<
export type RedwoodApolloLinks = [
RedwoodApolloLink<'withToken'>,
RedwoodApolloLink<'authMiddleware'>,
RedwoodApolloLink<'updateDataApolloLink'>,
RedwoodApolloLink<'enhanceErrorLink'>,
RedwoodApolloLink<'httpLink', HttpLink>
]

Expand Down
44 changes: 6 additions & 38 deletions packages/web/src/apollo/suspense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
import type {
ApolloCache,
ApolloClientOptions,
ApolloLink,
HttpOptions,
InMemoryCacheConfig,
setLogVerbosity,
ApolloLink,
} from '@apollo/client'
import {
setLogVerbosity as apolloSetLogVerbosity,
Expand All @@ -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'
Expand Down Expand Up @@ -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 = () => {
Expand All @@ -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({
Expand All @@ -181,30 +168,11 @@ const ApolloProviderWithFetchConfig: React.FunctionComponent<{

return (
<ApolloNextAppProvider makeClient={makeClient}>
<ErrorBoundary onError={extendErrorAndRethrow}>{children}</ErrorBoundary>
{children}
</ApolloNextAppProvider>
)
}

type ComponentDidCatch = React.ComponentLifecycle<any, any>['componentDidCatch']

interface ErrorBoundaryProps {
error?: unknown
onError: NonNullable<ComponentDidCatch>
children: React.ReactNode
}

class ErrorBoundary extends React.Component<ErrorBoundaryProps> {
componentDidCatch(...args: Parameters<NonNullable<ComponentDidCatch>>) {
this.setState({})
this.props.onError(...args)
}

render() {
return this.props.children
}
}

export const RedwoodApolloProvider: React.FunctionComponent<{
graphQLClientConfig?: GraphQLClientConfigProp
useAuth?: UseAuth
Expand Down
52 changes: 33 additions & 19 deletions packages/web/src/components/DevFatalErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -90,9 +98,7 @@ export const DevFatalErrorPage = (props: { error?: ErrorWithRequestMeta }) => {
))}
</div>
</div>
{props.error.mostRecentRequest ? (
<ResponseRequest error={props.error} />
) : null}
<ResponseRequest error={props.error} />
</section>
</main>
)
Expand Down Expand Up @@ -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 (
<div className="request-response">
{props.error.mostRecentRequest ? (
{mostRecentRequest ? (
<div>
<h4>Request: {props.error.mostRecentRequest.operationName}</h4>
<h4>Request: {mostRecentRequest.operationName}</h4>
<div>
<h5>Variables:</h5>
<code>
<pre>
{JSON.stringify(
props.error.mostRecentRequest.variables,
null,
' '
)}
{JSON.stringify(mostRecentRequest.variables, null, ' ')}
</pre>
</code>
</div>
Expand All @@ -250,13 +264,13 @@ function ResponseRequest(props: { error: ErrorWithRequestMeta }) {
onClick={() => setOpenQuery(!openQuery)}
className={openQuery ? 'open' : 'preview'}
>
{props.error.mostRecentRequest.query}
{mostRecentRequest.query}
</pre>
</code>
</div>
</div>
) : null}
{props.error.mostRecentRequest ? (
{mostRecentResponse ? (
<div className="response">
<h4>Response</h4>
<div>
Expand All @@ -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, ' ')}
</pre>
</code>
</div>
Expand Down

0 comments on commit 7870ce4

Please sign in to comment.