feat(fe2): more reslient app reboot on fatal network error (#5363)

* feat(fe2): more reslient app reboot on fatal network error

* minor adjustment
This commit is contained in:
Kristaps Fabians Geikins
2025-09-03 10:47:50 +03:00
committed by GitHub
parent 361aa523ac
commit 427117c15d
3 changed files with 63 additions and 10 deletions
@@ -27,6 +27,7 @@ import type { ActiveUserMainMetadataQuery } from '~~/lib/common/generated/gql/gr
import { useScopedState } from '~/lib/common/composables/scopedState'
import type { ApolloClient } from '@apollo/client/core'
import { AuthFailedError } from '~/lib/auth/errors/errors'
import { useAppErrorState } from '~/lib/core/composables/error'
type UseOnAuthStateChangeCallback = (
user: MaybeNullOrUndefined<ActiveUserMainMetadataQuery['activeUser']>,
@@ -220,6 +221,7 @@ export const useAuthManager = (
) => {
const { deferredApollo } = options || {}
const ssrEvent = useRequestEvent()
const apiOrigin = useApiOrigin()
const resetAuthState = useResetAuthState({ deferredApollo })
const route = useRoute()
@@ -230,6 +232,7 @@ export const useAuthManager = (
const postAuthRedirect = usePostAuthRedirect()
const { markLoggedOut } = useJustLoggedOutTracking()
const { logger } = useSafeLogger()
const { isFullRedirectState } = useAppErrorState()
/**
* Invite token, if any
@@ -259,6 +262,24 @@ export const useAuthManager = (
() => dashboardToken.value || embedToken.value || authToken.value
)
/**
* Trigger full redirect that causes a full reload, instead of an in-session navigation
*/
const sendFullRedirect = async (relativeUrl: string) => {
if (isFullRedirectState.value) return
isFullRedirectState.value = true
if (import.meta.client) {
window.location.href = relativeUrl
} else if (ssrEvent) {
const { sendRedirect } = await import('h3')
await sendRedirect(ssrEvent, relativeUrl)
} else {
logger().fatal('Failed to send full redirect')
}
}
/**
* Set/clear new token value and redirect to home
*/
@@ -513,10 +534,10 @@ export const useAuthManager = (
}
if (!options?.skipRedirect) {
if (options?.forceFullReload && import.meta.client) {
window.location.href = loginRoute
} else {
if (!options?.forceFullReload) {
await goToLogin()
} else {
await sendFullRedirect(loginRoute)
}
}
}
@@ -15,7 +15,8 @@ const ENTER_STATE_AT_ERRORS_PER_MIN = 100
export function useAppErrorState() {
const state = useScopedState('appErrorState', () => ({
inErrorState: ref(false),
errorRpm: Observability.simpleRpmCounter()
errorRpm: Observability.simpleRpmCounter(),
isFullRedirectState: ref(false)
}))
const nuxtApp = useNuxtApp()
@@ -32,7 +33,22 @@ export function useAppErrorState() {
)
state.inErrorState.value = true
}
}
},
/**
* Similar to error state, except we don't show any UI elements to the user, we just stop processing
* API calls etc. because we're redirecting fully away
*/
isFullRedirectState: state.isFullRedirectState,
/**
* Whether to prevent HTTP API calls
*/
preventHttpCalls: computed(() => state.isFullRedirectState.value),
/**
* Whether to prevent websocket messaging
*/
preventWebsocketMessaging: computed(
() => state.isFullRedirectState.value || state.inErrorState.value
)
}
}
+21 -5
View File
@@ -4,7 +4,7 @@ import { SubscriptionClient } from 'subscriptions-transport-ws'
import type { ApolloConfigResolver } from '~~/lib/core/nuxt-modules/apollo/module'
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs'
import { WebSocketLink } from '@apollo/client/link/ws'
import { getMainDefinition } from '@apollo/client/utilities'
import { getMainDefinition, Observable } from '@apollo/client/utilities'
import { Kind } from 'graphql'
import type { GraphQLError, OperationDefinitionNode } from 'graphql'
import type { CookieRef, NuxtApp } from '#app'
@@ -441,7 +441,23 @@ function createLink(params: {
logout: ReturnType<typeof useAuthManager>['logout']
}): ApolloLink {
const { httpEndpoint, wsClient, authToken, nuxtApp, reqId, logout } = params
const { registerError, isErrorState } = useAppErrorState()
const {
registerError,
preventHttpCalls,
preventWebsocketMessaging,
isFullRedirectState
} = useAppErrorState()
const stopLink = new ApolloLink((operation, forward) => {
if (preventHttpCalls.value) {
// swallow the req, we're blocking them all
return new Observable(() => {
return () => {}
})
}
return forward(operation)
})
const errorLink = onError((res) => {
const logger = nuxtApp.$logger
@@ -487,7 +503,7 @@ function createLink(params: {
}
const { networkError } = res
if (networkError && isInvalidAuth(networkError)) {
if (networkError && isInvalidAuth(networkError) && !isFullRedirectState.value) {
// Reset auth
// since this may happen mid-routing, a standard router.push call may not work - do full reload
void logout({ skipToast: true, forceFullReload: true })
@@ -577,7 +593,7 @@ function createLink(params: {
wsClient.use([
{
applyMiddleware: (_opt, next) => {
if (isErrorState.value) {
if (preventWebsocketMessaging.value) {
return // never invokes next() - essentially stuck
}
@@ -614,7 +630,7 @@ function createLink(params: {
})
})
return from([...(import.meta.server ? [loggerLink] : []), errorLink, link])
return from([stopLink, ...(import.meta.server ? [loggerLink] : []), errorLink, link])
}
const defaultConfigResolver: ApolloConfigResolver = () => {