apollo icon indicating copy to clipboard operation
apollo copied to clipboard

Custom error handling not firing

Open lopermo opened this issue 5 years ago • 53 comments
trafficstars

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)

lopermo avatar Mar 09 '20 11:03 lopermo

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 avatar Mar 17 '20 22:03 drewbaker

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.

drewbaker avatar Mar 19 '20 18:03 drewbaker

image

Errors...

rospirski avatar Mar 20 '20 13:03 rospirski

Would you mind explaining how did you set it up?

lopermo avatar Mar 20 '20 15:03 lopermo

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,

rospirski avatar Mar 20 '20 15:03 rospirski

@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

drewbaker avatar Mar 20 '20 23:03 drewbaker

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 avatar Mar 20 '20 23:03 dmitrijt9

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

drewbaker avatar Mar 20 '20 23:03 drewbaker

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

dmitrijt9 avatar Mar 20 '20 23:03 dmitrijt9

@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 avatar Mar 20 '20 23:03 rospirski

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

rospirski avatar Apr 07 '20 03:04 rospirski

@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.

dmitrijt9 avatar Apr 07 '20 10:04 dmitrijt9

@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 avatar Apr 07 '20 13:04 rospirski

@Akryum help pliz 👍

rospirski avatar Apr 07 '20 14:04 rospirski

@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.

drewbaker avatar Apr 07 '20 23:04 drewbaker

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.

xeno avatar Apr 20 '20 18:04 xeno

@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

alza54 avatar Jun 12 '20 14:06 alza54

@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

DanielKaviyani avatar Aug 05 '20 19:08 DanielKaviyani

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

wanxe avatar Aug 13 '20 16:08 wanxe

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

DanielKaviyani avatar Aug 13 '20 17:08 DanielKaviyani

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

wanxe avatar Aug 13 '20 19:08 wanxe

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

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

wanxe avatar Aug 13 '20 19:08 wanxe

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

SebasEC96 avatar Aug 18 '20 18:08 SebasEC96

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

DanielKaviyani avatar Aug 18 '20 19:08 DanielKaviyani

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!!!

SebasEC96 avatar Aug 18 '20 19:08 SebasEC96

@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()
  }
}`

DanielKaviyani avatar Aug 18 '20 19:08 DanielKaviyani

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

drewbaker avatar Aug 18 '20 19:08 drewbaker

@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 avatar Aug 18 '20 19:08 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

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!

SebasEC96 avatar Aug 18 '20 19:08 SebasEC96

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

KazW avatar Sep 09 '20 08:09 KazW