-
Notifications
You must be signed in to change notification settings - Fork 199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom error handling not firing #315
Comments
I'm seeing this too. Impossible to get access to the error object directly. It's locked as a string now, of this format
But error handler
|
I'm hoping once this works, I can catch a network error and handle a token refresh call. If anyone has any tips on a better way to handle seamless token refresh I'd love to hear it. |
Would you mind explaining how did you set it up? |
I'm trying to use the 'apollo' module on nuxt, but I have two problems. This query that I'm trying to access is limited, I give an apollo error with access denied.
Sorry for any typing mistakes, I am Brazilian and I will use the Google translator. But then, you can use asyncData and call a query using the this function. $ Apollo.query (...) Believe or solve problems in error handling in SSR, |
@rospirski I think your errors are unrelated to the error handler. I made a sandbox showing the error handler not working: |
Hi, try to add error handler to plugins section in your nuxt.config too. It works for me when using apollo module in component like you have in sandbox. |
@dmitrijt9 Can you share your config? Weird you need to define it in 2 places. |
@drewbaker I's weird for me either. Here it is:
|
I did a minimal here Remembering that it is necessary to access the page and give an F5, the rendering needs to be on the Server, not just on the client side. And as you can be the logs are always duplicated. Github with the project I used. |
As a solution I am using nuxt's asyncData ... but no solution yet? |
@drewbaker @rospirski So, I've mistaken previously. You don't have to mention apollo-error-handler in plugins in nuxt config, it's enough to write it in apollo config. But I noticed that apollo-error-handler triggers only on client side... Which is quite weird and not sure, that this is correct. There is one positive thing about it, that you can immediately show some error notification to the user etc. But I still think it should be triggering on server. |
So, if I use nuxt's asyncData, I can use the context error, resize the page for the error layout. An alternative would be to use both. I'll look somewhere to avoid showing the error |
@Akryum help pliz 👍 |
@rospirski thnaks! I can get the error handler to be used like you have it, but only in smart queries, not in mutations using |
Is there no solution for catching 400 errors from Apollo? Even something as simple as an email validation on a mutation only throws a global error. As @drewbaker mentioned, the ability to read the body of error messages on a 400 would be ideal. |
@drewbaker Hello, you can handle a token refresh call this way: // nuxt.config.ts
'apollo': {
'clientConfigs': {
'default': '~/apollo/client-configs/default.ts',
},
}, // apollo/client-configs/default.ts
async function fetchNewAccessToken (ctx: Context): Promise<string | undefined> {
await ctx.store.dispatch('auth/fetchAuthToken');
return ctx.store.state.auth.authToken;
}
function errorHandlerLink (ctx: Context): any {
return ApolloErrorHandler({
isUnauthenticatedError (graphQLError: GraphQLError): boolean {
const { extensions } = graphQLError;
return extensions?.exception?.message === 'Unauthorized';
},
'fetchNewAccessToken': fetchNewAccessToken.bind(undefined, ctx),
'authorizationHeaderKey': 'X-MyService-Auth',
});
}
export default function DefaultConfig (ctx: Context): unknown {
return {
'link': ApolloLink.from([errorHandlerLink(ctx)]),
'httpEndpoint': ctx.env.GRAPHQL_URL,
};
} // apollo/error-handler.ts
export default function ApolloErrorHandler ({
isUnauthenticatedError,
fetchNewAccessToken,
authorizationHeaderKey,
} : Options): any {
return onError(({
graphQLErrors,
networkError,
forward,
operation,
}) => {
if (graphQLErrors) {
for (const error of graphQLErrors) {
if (isUnauthenticatedError(error)) {
return new Observable(observer => {
fetchNewAccessToken()
.then(newAccessToken => {
if (!newAccessToken) {
throw new Error('Unable to fetch new access token');
}
operation.setContext(({ headers = {} }: any) => ({
'headers': {
...headers,
[authorizationHeaderKey]: newAccessToken || undefined,
},
}));
})
.then(() => {
const subscriber = {
'next': observer.next.bind(observer),
'error': observer.error.bind(observer),
'complete': observer.complete.bind(observer),
};
forward(operation).subscribe(subscriber);
})
.catch(fetchError => {
observer.error(fetchError);
});
});
}
}
} else if (networkError) {
// ...
}
});
} See https://github.com/baleeds/apollo-link-refresh-token |
@Akryum |
Yes, same here... |
hi |
Seems good but, how I can do that on nuxt? |
Oh I see! using the client config... thanks |
How did you manage to redirect to the login page? It tells me that "router is not defined" when i try to do a router.push |
you most use redirect method |
Thanks!!! |
@SebasEC96
|
FYI for gave up on Apollo and switched to this, works way better with Nuxt in my opinion: https://github.com/Gomah/nuxt-graphql-request |
Yes, it took me a while to realize it, that's why I deleted the message, thanks! |
It depends on your needs, it does not use the cache among other things, but I will save it in case i need it at any time, thanks! |
After finding this issue and searching through the source code of this library, It's not super pretty and does duplicate some code in this library, but it shows how to completely customize the Apollo client. |
you can set the errorPolicy property in the apollo query to catch errors
|
Thanks @KazW I ended up using a version of your code to get my refresh tokens working using Nuxt-Apollo, Nuxt-Auth (dev), Hasura and Auth0.
|
Seems the error for errorHandler is not resolved |
I still think its weird that the error-handler that you can pass to the apollo config isn't fired when doing this.$apollo.query/mutate. Only on smart queries. Makes no sense to me. |
How is this still not fixed? It's a breaking bug that makes Apollo completely unusable with Nuxt as you cannot handle an expired token any other way that I've seen. |
any plans to fix this issue? |
Sorry everyone for late reponse. Currently, I have some changed in my job, that make me not working with coding for a while. I'm not sure why custom errorHandler is not firing. For now i'm not sure if it const vueApolloOptions = Object.assign(providerOptions, {
...
errorHandler (error) {
<% if (options.errorHandler) { %>
return require('<%= options.errorHandler %>').default(error, ctx)
<% } else { %>
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
<% } %>
}
})
const apolloProvider = new VueApollo(vueApolloOptions) About the refreshToken. I did some research about refreshToken recently, as the latest update of Akryum/vue-cli-plugin-apollo#243 if (preAuthLinks.length) {
link = from(preAuthLinks).concat(authLink)
} You guy may can try that options, combine with https://github.com/newsiberian/apollo-link-token-refresh maybe? I'm looking for a solution for this, and if anyone know what is happening, a PR for this is more wellcome :) |
It seems the vue-apollo doesn't use anymore the ApolloClient.errorHandler() to handle errors, which is still used on @nuxtjs/apollo...
|
@mellson thanks for this. I'm also using Hasura! I've found one improvement to your code that was a bug for me. When you have: const wsLink = new WebSocketLink(wsClient)
if (process.server) {
link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
link
)
} else {
link = from([wsLink])
} I've found that I have some requests where I set custom Hasura headers ( if (process.server) {
link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
link
)
} else {
link = split(
({ query, getContext }) => {
const { kind, operation } = getMainDefinition(query)
const { headers } = getContext()
const hasHasuraHeaders = Object.keys(headers).some(header =>
header.toLowerCase().includes('x-hasura')
)
return !hasHasuraHeaders
},
wsLink,
link
)
} It's basically if on the client side and we have custom hasura headers, send a http (where custom headers are pushed through) and not a websocket. Hope this helps! |
Oh one more amendment! This is to ensure the Auth header isn't sent through after the user has explicitly logged out: const authLink = setContext(async (_, { headers }) => {
const Authorization = await getAuthToken()
const authorizationHeader = Authorization ? { Authorization } : {}
// delete the existing Authorization header if they have logged out
if (!context.$auth.loggedIn) delete headers.Authorization
return {
headers: {
...headers,
// overwrite the Authorization header with the new one
...authorizationHeader,
},
}
}) |
@toddheslin nice, thanks 🙏🏻 |
It seems, let alone refreshing the token, this module is completely broken if you can't at least call onLogout() when it's expired.... @KazW I'm not sure how you got this to work as error handlers are not supposed to return a promise, and it basically throws on retriedResult = errorHandler({
graphQLErrors: result.errors,
response: result,
operation: operation,
forward: forward,
});
if (retriedResult) {
retriedSub = retriedResult.subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer),
});
return;
} I'm wondering: is there a specific reason why no one tried to fix this for everyone by submitting a PR since march ? @kieusonlam @KazW @toddheslin @mellson please, you all seem to have spent a while on this: any chance you could contribute to fix this once and for all ? I'm not sure I understand the whole issue, but FYI the only working workaround on my end is this in a dedicated export default function ({ redirect, app, env }) {
const httpEndpoint = `${env.BASE_URL}/graphql`
const link = onError(({ graphQLErrors, networkError, operation, forward }) => {
console.log(graphQLErrors)
if (graphQLErrors && graphQLErrors[0].message.includes('UNAUTHORIZED')) {
app.$cookies.remove('apollo-token')
redirect('/')
}
return forward(operation)
})
return {
link,
httpEndpoint,
}
} |
Apollo is on v3 and whatever is used for the config is using the old v2. |
I feel your pain @dylanmcgowan I noticed that my solution above has another flaw: the subscription Here is my current setup for anyone who needs the fix immediately. I'll look at a PR that might allow a more flexible creation.
import { from, split } from 'apollo-link'
import { setContext } from 'apollo-link-context'
import { WebSocketLink } from 'apollo-link-ws'
import { createUploadLink } from 'apollo-upload-client'
import { getMainDefinition } from 'apollo-utilities'
import { SubscriptionClient } from 'subscriptions-transport-ws'
import { SentryLink } from 'apollo-link-sentry'
import * as ws from 'ws'
/**
* https://github.com/nuxt-community/apollo-module
* https://github.com/nuxt-community/apollo-module/issues/315#issuecomment-711156190
*/
// eslint-disable-next-line import/no-mutable-exports
export let wsClient
export default context => {
// See options: https://www.npmjs.com/package/apollo-link-sentry
const sentryLink = new SentryLink()
const WS_URL = context.$config.BASE_URL.replace('http', 'ws')
let link
const httpLink = createUploadLink({
uri: `${context.$config.BASE_URL}/gql`,
})
link = from([sentryLink, httpLink])
const getAuthToken = async () => {
const auth = context.$auth.strategy
if (await auth.token.status().expired()) {
// eslint-disable-next-line no-console
context.$sentry.addBreadcrumb({
category: 'auth',
message: 'Token expired, refreshing',
level: context.Sentry.Severity.Info,
})
await auth.refreshController.handleRefresh()
}
return auth.token.get()
}
const authLink = setContext(async (_, { headers }) => {
const Authorization = await getAuthToken()
const authorizationHeader = Authorization ? { Authorization } : {}
// delete the existing Authorization header if they have logged out
if (!context.$auth.loggedIn) delete headers.Authorization
return {
headers: {
...headers,
// overwrite the Authorization header with the new one
...authorizationHeader,
},
}
})
link = authLink.concat(link)
wsClient = new SubscriptionClient(
`${WS_URL}/gql`,
{
reconnect: true,
lazy: true,
connectionParams: async () => {
const Authorization = await getAuthToken()
return Authorization
? { Authorization, headers: { Authorization } }
: {}
},
},
process.server ? ws : WebSocket
)
const wsLink = new WebSocketLink(wsClient)
if (process.server) {
link = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
link
)
} else {
link = split(
({ getContext }) => {
const { headers } = getContext()
const hasHasuraHeaders = Object.keys(headers).some(header =>
header.toLowerCase().includes('x-hasura')
)
return !hasHasuraHeaders
},
wsLink,
link
)
}
return {
defaultHttpLink: false,
wsClient,
link,
}
}
import {
provide,
onGlobalSetup,
defineNuxtPlugin,
} from '@nuxtjs/composition-api'
import { DefaultApolloClient } from '@vue/apollo-composable'
import { restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
import { wsClient } from './config'
/**
* This plugin will connect @nuxt/apollojs with @vue/apollo-composable
*/
export default defineNuxtPlugin(({ app, $apolloHelpers }) => {
const defaultOnLogin = $apolloHelpers.onLogin
onGlobalSetup(() => {
provide(DefaultApolloClient, app.apolloProvider.defaultClient)
$apolloHelpers.onLogin = function modifiedOnLogin() {
defaultOnLogin()
restartWebsockets(wsClient)
}
})
})
export default {
plugins: ['~/plugins/apollo/plugin.js'],
apollo: {
clientConfigs: {
default: '~/plugins/apollo/config.js',
},
cookieAttributes: {
httpOnly: false,
sameSite: 'Strict',
secure: true,
},
},
} |
All, I'm not sure about the redirect issue, as my login redirects are automatically handled with @nuxtjs/auth-next. However, I was able to get the output of the 400 errors using the following: In
And then setting up in
I know someone had already mentioned this earlier in this giant thread but hopefully for the few brave souls venturing this far, I can confirm this solution at least allows visibility on Apollo errors like 400. |
have problem with reading docs, it says to put |
could anyone call
apollo: {
clientConfigs: {
default: '~~/apollo/default.ts',
},
errorHandler: '~~/apollo/error.ts',
},
import type { Context } from '@nuxt/types'
import type { ErrorResponse } from 'apollo-link-error'
import consola from 'consola'
const logger = consola.withTag('apollo')
const errorHandler: (resp: ErrorResponse, ctx: Context) => void = (
{ graphQLErrors, networkError },
{ error },
) => {
if (process.server) {
if (graphQLErrors) {
graphQLErrors.forEach((err) => {
// Works.
logger.error(err)
})
const message = graphQLErrors
.map((err) => `GraphQL error: ${err.message} @ ${err.path.join('/')}`)
.join(', ')
// This doesn't works.
error({
statusCode: 400,
message,
})
}
if (networkError) {
logger.error(networkError)
// This doesn't works.
error({
statusCode: 500,
message: `${networkError.name}: ${networkError.message}`,
})
}
}
}
export default errorHandler |
just found |
For me it shows an unresponsive empty screen with the navbar. Dev environment works fine. Is that the same as for you? I noticed that setting Edit: the unresponsive page is caused by the issue below |
I actually get an error message in the console when it should show the error component:
Edit: |
so what i found so far is; https://github.com/nuxt/nuxt.js/blob/v2.15.4/packages/vue-app/template/server.js#L258-L260
this makes impossible to call @Migushthe2nd this looks like what i call "hydration error" |
@kieusonlam @atinux any ideas how we can address this issue? error handling with |
Can this same technique be used for firebase auth with JWT while using Hasura ?? |
Version
v4.0.0-rc.19
Reproduction link
https://jsfiddle.net/
Steps to reproduce
Add option to nuxt.config.js -> errorHandler: '~/plugins/apollo-error-handler.js',
Create file and print error.
What is expected ?
It should print errors on the console
What is actually happening?
It doesn't print anything when an error happens.
Additional comments?
I'm trying to catch errors when the connection to the server is lost and there's a subscription ongoing. But I can't even catch and log when the server isn't connected and I try to run a query. It's like if the file in "errorHandler" option is ignored.
The text was updated successfully, but these errors were encountered: