nuxt-modules / apollo

Nuxt.js module to use Vue-Apollo. The Apollo integration for GraphQL.
https://apollo.nuxtjs.org
MIT License
944 stars 198 forks source link

Custom error handling not firing #315

Open lopermo opened 4 years ago

lopermo commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

image

Errors...

lopermo commented 4 years ago

Would you mind explaining how did you set it up?

rospirski commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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

dmitrijt9 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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

dmitrijt9 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

@Akryum help pliz 👍

drewbaker commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

@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 commented 4 years ago

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

DanielKaviyani commented 4 years ago

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 commented 4 years ago

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

wanxe commented 4 years ago

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

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

SebasEC96 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

Seems the error for errorHandler is not resolved

rnenjoy commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

any plans to fix this issue?

kieusonlam commented 3 years ago

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

https://github.com/Akryum/vue-cli-plugin-apollo/pull/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 commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

@toddheslin nice, thanks 🙏🏻

syffs commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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.