Skip to content
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

Open
lopermo opened this issue Mar 9, 2020 · 53 comments
Open

Custom error handling not firing #315

lopermo opened this issue Mar 9, 2020 · 53 comments

Comments

@lopermo
Copy link

lopermo commented Mar 9, 2020

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.

This bug report is available on Nuxt community (#c299)
@drewbaker
Copy link

I'm seeing this too. Impossible to get access to the error object directly. It's locked as a string now, of this format Error: GraphQL error: {...}.

    apollo: {
        errorHandler: "~/plugins/apollo-error-handler.js",
        clientConfigs: {
            default: "~/plugins/apollo-config-default.js"
        }
    }

But error handler apollo-error-handler.js is this:

export default (
    { graphQLErrors, networkError, operation, forward },
    nuxtContext
) => {
    console.log("Global error handler")
    console.log(graphQLErrors, networkError, operation, forward)
}

@drewbaker
Copy link

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.

@rospirski
Copy link

image

Errors...

@lopermo
Copy link
Author

lopermo commented Mar 20, 2020

Would you mind explaining how did you set it up?

@rospirski
Copy link

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.
Whenever I try to make a query it returns the value in the SSR, and twice in the client

image

This query that I'm trying to access is limited, I give an apollo error with access denied.
Breaking AI leaves this error more.

vue.runtime.esm.js?2b0e:619 [Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

image

<script> import gql from 'graphql-tag' export default { name: 'Teste', apollo: { account: { query: gql { account { id login real_name email telefone zipcode create_time status availDt last_play cash mileage avatar capa pais roles } } , update(data) { console.log(data) return data.account }, deep: false, prefetch: true, fetchPolicy: 'network-only' } }, data() { return { account: null } } } </script>

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 (...)
It works normally, so there are errors with then / catch.

Believe or solve problems in error handling in SSR,

@drewbaker
Copy link

@rospirski I think your errors are unrelated to the error handler.

I made a sandbox showing the error handler not working:
https://codesandbox.io/s/apollo-broken-error-handler-499o7

@dmitrijt9
Copy link

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.

@drewbaker
Copy link

@dmitrijt9 Can you share your config? Weird you need to define it in 2 places.

@dmitrijt9
Copy link

dmitrijt9 commented Mar 20, 2020

@drewbaker I's weird for me either. Here it is:

require('dotenv').config()

module.exports = {
  mode: 'universal',
  /*
  ** Headers of the page
  */
  head: {
    title: process.env.npm_package_name || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress-bar color
  */
  loading: { color: '#fff' },
  /*
  ** Global CSS
  */
  css: [
    '~/assets/css/tailwind.css',
    '@fortawesome/fontawesome-svg-core/styles.css'
  ],

  tailwindcss: {
    cssPath: '~/assets/css/tailwind.css',
  },
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/fontawesome.js',
    '~/plugins/apollo-error-handler.js'
  ],
  /*
  ** Nuxt.js dev-modules
  */
  buildModules: [
    // Doc: https://github.com/nuxt-community/eslint-module
    '@nuxtjs/eslint-module',
    // Doc: https://github.com/nuxt-community/nuxt-tailwindcss
    '@nuxtjs/tailwindcss',
    // Doc: https://github.com/nuxt-community/dotenv-module
    '@nuxtjs/dotenv',
  ],
  // dotenv options
  dotenv: {
    path: '../../' // point to global .env file
  },

  eslint: {
    fix: true
  },

  router: {
    middleware: ['auth']
  },

  proxy: {
    '/api': {
      target: 'http://localhost:4000',
      pathRewrite: {'^/api': '/'}
    },
    '/api/playground': {
      target: 'http://localhost:4000/playground',
      pathRewrite: {'^/api': '/'}
    }
  },
  /*
  ** Nuxt.js modules
  */
  modules: [
    '@nuxtjs/apollo',
    '@nuxtjs/proxy',
    '@nuxtjs/toast',
    [
      'nuxt-i18n',
      {
        defaultLocale: 'en',
        locales: [
          { code: 'cs', iso: 'cs-CZ', file: 'cs.json'},
          { code: 'en', iso: 'en-Us', file: 'en.json'}
        ],
        lazy: true,
        langDir: 'translations/',
        parsePages: false,
        pages: {
          about: {
            cs: '/o-aplikaci',
            en: '/about'
          },
          app: {
            cs: '/app',
            en: '/app',
          },
          'app/dashboard': {
            cs: '/app/nastenka',
            en: '/app/dashboard'
          },
          'app/calendar': {
            cs: '/app/kalendar',
            en: '/app/calendar'
          },
          'app/tasks': {
            cs: '/app/ukoly',
            en: '/app/tasks'
          },
          'app/team': {
            cs: '/app/tym',
            en: '/app/team'
          },
          'app/discussion': {
            cs: '/app/diskuze',
            en: '/app/discussion'
          },
        }
      }
    ]
  ],
  // Apollo config
  apollo: {
    tokenName: 'apollo-token',
    cookieAttributes: {
      secure: process.env.ENV !== 'dev',
      expires: 365,// cookie expiration 1 year
      path: '/'
    },
    clientConfigs: {
      default: {
        httpEndpoint: 'http://localhost:4000',
        browserHttpEndpoint: '/api'
      }
    },
    errorHandler: '~/plugins/apollo-error-handler.js'
  },

  toast: {
    position: 'top-right',
    duration: 5000,
    action: {
      text: 'X',
      onClick : (e, toastObject) => {
        toastObject.goAway(0);
      },
      class: 'notification'
    },
    containerClass: 'theme-light',
    className: 'notification'
  },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
    }
  }
}

@rospirski
Copy link

@rospirski I think your errors are unrelated to the error handler.

I made a sandbox showing the error handler not working:
https://codesandbox.io/s/apollo-broken-error-handler-499o7

I did a minimal here
If I access the link '/ test1' it works normally because there is no error
Now if I access '/ teste2 /' as it generates an ApolloError in the API, this error simply appears on the console, I can treat it as you ordered, but even so it still generates the error.

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.

image

image

And as you can be the logs are always duplicated.

Github with the project I used.
https://github.com/rospirski/Apoll-Nuxt-Problem

@rospirski
Copy link

As a solution I am using nuxt's asyncData ... but no solution yet?

@dmitrijt9
Copy link

@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...
And it triggers only when using apollo smart query.

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.

@rospirski
Copy link

@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...
And it triggers only when using apollo smart query.

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.
SmartQuery and AsyndData, however it would be two requests.

I'll look somewhere to avoid showing the error
a if(process.client)

@rospirski
Copy link

@Akryum help pliz 👍

@drewbaker
Copy link

@rospirski thnaks! I can get the error handler to be used like you have it, but only in smart queries, not in mutations using this.$apollo.mutate(), then it will use the generic error handler (which makes it impossible to do things like "${error.details.field} input not provided" messages.

@xeno
Copy link

xeno commented Apr 20, 2020

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.

@alza54
Copy link

alza54 commented Jun 12, 2020

@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

@DanielKaviyani
Copy link

@Akryum
Is there no solution to this problem?
How can we manage errors?
Especially when we have 401 errors
I have to redirect the user to the login page

@wanxe
Copy link

wanxe commented Aug 13, 2020

Yes, same here...
Some have found a workaround to be able to intercept the graphql errors?

@DanielKaviyani
Copy link

Yes, same here...
Some have found a workaround to be able to intercept the graphql errors?

hi
I handled it using apollo-link-error in apollo-config.js
source:
https://v4.apollo.vuejs.org/guide-composable/error-handling.html#network-errors

@wanxe
Copy link

wanxe commented Aug 13, 2020

Seems good but, how I can do that on nuxt?

@wanxe
Copy link

wanxe commented Aug 13, 2020

Seems good but, how I can do that on nuxt?

Oh I see! using the client config... thanks

@SebasEC96
Copy link

Yes, same here...
Some have found a workaround to be able to intercept the graphql errors?

hi
I handled it using apollo-link-error in apollo-config.js
source:
https://v4.apollo.vuejs.org/guide-composable/error-handling.html#network-errors

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

@DanielKaviyani
Copy link

Yes, same here...
Some have found a workaround to be able to intercept the graphql errors?

hi
I handled it using apollo-link-error in apollo-config.js
source:
https://v4.apollo.vuejs.org/guide-composable/error-handling.html#network-errors

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
in plugins/apollo-config.js :
export default function ({ redirect }) { redirect('/auth/login') }

@SebasEC96
Copy link

Yes, same here...
Some have found a workaround to be able to intercept the graphql errors?

hi
I handled it using apollo-link-error in apollo-config.js
source:
https://v4.apollo.vuejs.org/guide-composable/error-handling.html#network-errors

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
in plugins/apollo-config.js :
export default function ({ redirect }) { redirect('/auth/login') }

Thanks!!!

@DanielKaviyani
Copy link

DanielKaviyani commented Aug 18, 2020

@SebasEC96
move your code into the function
before return

export default function ({ redirect }) {
  const link = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.map(({ message }) => {
      if (`${message}` === 'Unauthenticated.') {
        redirect('/login')
        // Do Something
        localStorage.setItem('logged', false)
      }
    })
  }
  if (networkError) {
    console.log(`[Network error]: ${networkError}`)
  }
})
  return {
    defaultHttpLink: false,
    link: ApolloLink.from([link, createHttpLink({
      credentials: 'include',
      uri: 'http://localhost:8000/graphql',
      fetch: (uri, options) => {
        options.headers['X-XSRF-TOKEN'] = Cookies.get('XSRF-TOKEN')
        return fetch(uri, options)
      }
    })]),
    cache: new InMemoryCache()
  }
}`

@drewbaker
Copy link

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

@SebasEC96
Copy link

@SebasEC96
move your code into the function
before return

export default function ({ redirect }) {
  const link = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.map(({ message }) => {
      if (`${message}` === 'Unauthenticated.') {
        redirect('/login')
        // Do Something
        localStorage.setItem('logged', false)
      }
    })
  }
  if (networkError) {
    console.log(`[Network error]: ${networkError}`)
  }
})
  return {
    defaultHttpLink: false,
    link: ApolloLink.from([link, createHttpLink({
      credentials: 'include',
      uri: 'http://localhost:8000/graphql',
      fetch: (uri, options) => {
        options.headers['X-XSRF-TOKEN'] = Cookies.get('XSRF-TOKEN')
        return fetch(uri, options)
      }
    })]),
    cache: new InMemoryCache()
  }
}`

Yes, it took me a while to realize it, that's why I deleted the message, thanks!

@SebasEC96
Copy link

SebasEC96 commented Aug 18, 2020

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

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!

@KazW
Copy link

KazW commented Sep 9, 2020

After finding this issue and searching through the source code of this library, vue-apollo and subscriptions-transport-ws, I was able to come up with a a way to handle token refreshes (only logging out in my example) and network errors from sockets and requests, on the server and the client. I was very close to taking @drewbaker's advice and switching libraries.

It's not super pretty and does duplicate some code in this library, but it shows how to completely customize the Apollo client.
https://gist.github.com/KazW/2b5e4cb8f43566a69d3917ee7f30dbcc

@lizardopc
Copy link

you can set the errorPolicy property in the apollo query to catch errors
https://www.apollographql.com/docs/react/data/error-handling/

query: contentQuery, errorPolicy: 'all', variables () { return { Page: 'test' } },

@mellson
Copy link

mellson commented Oct 18, 2020

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.

// apollo-client-config.js (set this as the default client config for apollo in nuxt.config.js)
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 * as ws from 'ws'

export default (context) => {
  let link

  const httpLink = createUploadLink({
    uri: process.env.APOLLO_ENDPOINT,
  })
  link = from([httpLink])

  const getAuthToken = async () => {
    const auth = context.$auth.strategy
    if (await auth.token.status().expired()) {
      // eslint-disable-next-line no-console
      console.log('Token expired, refreshing')
      await auth.refreshController.handleRefresh()
    }

    return await auth.token.get()
  }

  const authLink = setContext(async (_, { headers }) => {
    const Authorization = await getAuthToken()
    const authorizationHeader = Authorization ? { Authorization } : {}
    return {
      headers: {
        ...headers,
        ...authorizationHeader,
      },
    }
  })
  link = authLink.concat(link)

  const wsClient = new SubscriptionClient(
    process.env.APOLLO_WSS_ENDPOINT,
    {
      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 = from([wsLink])
  }

  return {
    defaultHttpLink: false,
    link,
  }
}

@andrewbenrichard
Copy link

Seems the error for errorHandler is not resolved

@rnenjoy
Copy link

rnenjoy commented Nov 17, 2020

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.

@alrightsure
Copy link

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.

@ikasianiuk
Copy link

any plans to fix this issue?

@kieusonlam
Copy link
Collaborator

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 vue-apollo workflow or maybe lodash template use in nuxt module.

https://github.com/nuxt-community/apollo-module/blob/master/lib/templates/plugin.js#L95-L113

  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 vue-apollo-plugin-cli we have an option call preAuthLinks

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 :)

@brunocordioli072
Copy link

brunocordioli072 commented Nov 25, 2020

It seems the vue-apollo doesn't use anymore the ApolloClient.errorHandler() to handle errors, which is still used on @nuxtjs/apollo...
The way I used to fix this problem:

// apollo-config.js
import { onError } from "@apollo/client/link/error";

export default function(context) {
  const httpEndpoint = "http://localhost:4000/local/graphql";

  const link = onError(({ graphQLErrors }) => {
    graphQLErrors.forEach(err => {
      // do things
    });
  });

  return {
    link,
    httpEndpoint,
  };
}

// nuxt.config.js
{
  apollo: {
    clientConfigs: {
      default: "~/plugins/apollo-config.js" 
    },
  },
};

@toddheslin
Copy link

@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 (x-hasura-whatever) for my auth rules. So I've updated that part to:

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!

@toddheslin
Copy link

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,
      },
    }
  })

@mellson
Copy link

mellson commented Dec 3, 2020

@toddheslin nice, thanks 🙏🏻

@syffs
Copy link

syffs commented Dec 7, 2020

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.subscribe if they do because of this in node_modules/@apollo/client/link/error/error.cjs.js:

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 apollo-config.js using cookie-universal and dotenv-module to load BASE_URL on client-side (I know it's ugly as hell, but it works until I find a better option):

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

@dylanmcgowan
Copy link

Apollo is on v3 and whatever is used for the config is using the old v2. vue-apollo is also on v4 now and this apollo-module uses vue-apollo@3.. can we please get this sorted out??? The issue has been live since March (8 months). I would like to handle custom apollo errors in my nuxt app please

@toddheslin
Copy link

toddheslin commented Dec 19, 2020

I feel your pain @dylanmcgowan

I noticed that my solution above has another flaw: the subscription connectionParams() function is only being called when the subscription is initialized but not after login. So it's not reconnecting after I login with the new credentials. The core plugin only handles refreshing the connection if you pass in the wsURL:
https://github.com/nuxt-community/apollo-module/blob/v4.0.1-rc.5/lib/templates/plugin.js#L138

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.

plugins/apollo/config.js

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

plugins/apollo/plugin.js

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

nuxt.config.js

export default {
  plugins: ['~/plugins/apollo/plugin.js'],
  apollo: {
    clientConfigs: {
      default: '~/plugins/apollo/config.js',
    },
    cookieAttributes: {
      httpOnly: false,
      sameSite: 'Strict',
      secure: true,
    },
  },
}

@joshjung
Copy link

joshjung commented Apr 1, 2021

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 nuxt.config.apollo.js:

import { onError } from '@apollo/client/link/error'

export default ({ $config }) => {
  const link = onError(({ graphQLErrors, networkError }) => {
    if (graphQLErrors) {
      graphQLErrors.forEach(({ message, locations, path }) => {
        console.error(`[GraphQL error]: Message: ${message}`);
        console.error(`[GraphQL error]: Locations: ${JSON.stringify(locations)}`);
        console.error(`[GraphQL error]: Path: ${path}`);
      });
    }

    if (networkError) console.error(`[Network error]: ${networkError}`)
  })

  return {
    link,
    httpEndpoint: 'blah'
  }
}

And then setting up in nuxt.config.js:

apollo: {  
    clientConfigs: {
      default: '~/nuxt.config.apollo.js'
    }
},

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.

@victororlyk
Copy link

have problem with reading docs, it says to put
errorHandler inside of apolloConfig but then it says that you can't edit new config anymore. Adding it in apollo config didn't call the error, only adding it to plugins array did the trick.

@japboy
Copy link

japboy commented May 12, 2021

could anyone call error function from ctx in the error handler? error handler seems to work but error function cannot be called. i need to change error status code in circumstances. any help? thanks.

~~/nuxt.config.ts:

  apollo: {
    clientConfigs: {
      default: '~~/apollo/default.ts',
    },
    errorHandler: '~~/apollo/error.ts',
  },

~~/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

@japboy
Copy link

japboy commented May 14, 2021

just found errorPolicy: 'all' could avoid the issue above. this makes me allow to call error() and nuxt shows the error page. although it causes hydration error which doesn't show the error page properly in production build... 😭

@Migushthe2nd
Copy link

Migushthe2nd commented May 14, 2021

it causes hydration error which doesn't show the error page properly in production build... 😭

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 prefetch: false will fix the error component not being shown, but my pages need to prefetch in order to get the page's head ready for services that use the open graph protocol.

Edit: the unresponsive page is caused by the issue below

@Migushthe2nd
Copy link

Migushthe2nd commented May 14, 2021

I actually get an error message in the console when it should show the error component:

b5562e0.js:2 DOMException: Failed to execute 'appendChild' on 'Node': This node type does not support this method.
    at Object.appendChild (http://localhost:4000/_nuxt/b5562e0.js:2:41273)
    at y (http://localhost:4000/_nuxt/b5562e0.js:2:54196)
    at http://localhost:4000/_nuxt/b5562e0.js:2:53453
    at h (http://localhost:4000/_nuxt/b5562e0.js:2:53685)
    at _ (http://localhost:4000/_nuxt/b5562e0.js:2:54282)
    at D (http://localhost:4000/_nuxt/b5562e0.js:2:57668)
    at l.__patch__ (http://localhost:4000/_nuxt/b5562e0.js:2:58080)
    at l.t._update (http://localhost:4000/_nuxt/b5562e0.js:2:35019)
    at l.r (http://localhost:4000/_nuxt/b5562e0.js:2:65483)
    at bn.get (http://localhost:4000/_nuxt/b5562e0.js:2:27195)

b5562e0.zip

Edit:
I tried to track down the issue by adding breakpoints. For some reason this error is shown when this component get appended. If I comment the opening and closing tag the error isn't shown and I'm still able to navigate the website.

@japboy
Copy link

japboy commented May 18, 2021

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

if (ssrContext.nuxt.error) { ... } won't catch asynchrounous process which apollo error handler calls error() function updating ssrContext.nuxt.error.

this makes impossible to call error() from apollo error handler properly. i guess nuxt needs to change its error guard interface to support apollo error handler or similar asynchrounous process based libraries...?


@Migushthe2nd this looks like what i call "hydration error"

#315 (comment)

@khawarizmus
Copy link

@kieusonlam @atinux any ideas how we can address this issue? error handling with vue-apollo seems broken with ssr and it's very important for production apps to get it right.

@wackyapps
Copy link

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.

// apollo-client-config.js (set this as the default client config for apollo in nuxt.config.js)
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 * as ws from 'ws'

export default (context) => {
  let link

  const httpLink = createUploadLink({
    uri: process.env.APOLLO_ENDPOINT,
  })
  link = from([httpLink])

  const getAuthToken = async () => {
    const auth = context.$auth.strategy
    if (await auth.token.status().expired()) {
      // eslint-disable-next-line no-console
      console.log('Token expired, refreshing')
      await auth.refreshController.handleRefresh()
    }

    return await auth.token.get()
  }

  const authLink = setContext(async (_, { headers }) => {
    const Authorization = await getAuthToken()
    const authorizationHeader = Authorization ? { Authorization } : {}
    return {
      headers: {
        ...headers,
        ...authorizationHeader,
      },
    }
  })
  link = authLink.concat(link)

  const wsClient = new SubscriptionClient(
    process.env.APOLLO_WSS_ENDPOINT,
    {
      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 = from([wsLink])
  }

  return {
    defaultHttpLink: false,
    link,
  }
}

Can this same technique be used for firebase auth with JWT while using Hasura ??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests