mirror of
https://github.com/YunoHost/yunohost-admin.git
synced 2024-09-03 20:06:15 +02:00
refactor: APIError handling
This commit is contained in:
parent
254e1aca56
commit
9f8ee2f250
7 changed files with 85 additions and 110 deletions
|
@ -3,15 +3,16 @@
|
|||
* @module api/errors
|
||||
*/
|
||||
|
||||
import type { APIRequest } from '@/composables/useRequests'
|
||||
import i18n from '@/i18n'
|
||||
import type { APIErrorData, RequestMethod, APIRequest } from './api'
|
||||
import type { APIErrorData, RequestMethod } from './api'
|
||||
|
||||
class APIError extends Error {
|
||||
name = 'APIError'
|
||||
code: number
|
||||
status: string
|
||||
method: RequestMethod
|
||||
request: APIRequest
|
||||
requestId: string
|
||||
path: string
|
||||
|
||||
constructor(
|
||||
|
@ -28,7 +29,7 @@ class APIError extends Error {
|
|||
this.code = status
|
||||
this.status = statusText
|
||||
this.method = request.method
|
||||
this.request = request
|
||||
this.requestId = request.id
|
||||
this.path = urlObj.pathname + urlObj.search
|
||||
}
|
||||
|
||||
|
@ -152,13 +153,13 @@ const errors = {
|
|||
}
|
||||
|
||||
export {
|
||||
errors as default,
|
||||
APIError,
|
||||
APIErrorLog,
|
||||
APIBadRequestError,
|
||||
APIConnexionError,
|
||||
APIError,
|
||||
APIErrorLog,
|
||||
APIInternalError,
|
||||
APINotFoundError,
|
||||
APINotRespondingError,
|
||||
APIUnauthorizedError,
|
||||
errors as default,
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* @module api/handlers
|
||||
*/
|
||||
|
||||
import errors, { APIError } from '@/api/errors'
|
||||
import errors from '@/api/errors'
|
||||
import {
|
||||
STATUS_VARIANT,
|
||||
type APIRequest,
|
||||
|
@ -98,39 +98,6 @@ export function getError(
|
|||
errorCode = 'log'
|
||||
}
|
||||
|
||||
// This error can be catched by a view otherwise it will be catched by the `onUnhandledAPIError` handler.
|
||||
// This error can be catched by a view otherwise it will be catched by the global error handler.
|
||||
return new errors[errorCode](request, response, errorData)
|
||||
}
|
||||
|
||||
/**
|
||||
* If an APIError is not catched by a view it will be dispatched to the store so the
|
||||
* error can be displayed in the error modal.
|
||||
*/
|
||||
export function onUnhandledAPIError(error: APIError) {
|
||||
error.log()
|
||||
store.dispatch('HANDLE_ERROR', error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Global catching of unhandled promise's rejections.
|
||||
* Those errors (thrown or rejected from inside a promise) can't be catched by
|
||||
* `window.onerror`.
|
||||
*/
|
||||
export function registerGlobalErrorHandlers() {
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
const error = e.reason
|
||||
if (error instanceof APIError) {
|
||||
onUnhandledAPIError(error)
|
||||
// Seems like there's a bug in Firefox and the error logging in not prevented.
|
||||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
// Keeping this in case it is needed.
|
||||
|
||||
// Global catching of errors occuring inside vue components.
|
||||
// Vue.config.errorHandler = (err, vm, info) => {}
|
||||
|
||||
// Global catching of regular js errors.
|
||||
// window.onerror = (message, source, lineno, colno, error) => {}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
export { default, objectToParams } from './api'
|
||||
export { getError, registerGlobalErrorHandlers } from './handlers'
|
||||
export { getError } from './handlers'
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { createGlobalState } from '@vueuse/core'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { computed, reactive, shallowRef } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import type { APIQuery, RequestMethod } from '@/api/api'
|
||||
import { type APIError } from '@/api/errors'
|
||||
import { APIErrorLog, type APIError } from '@/api/errors'
|
||||
import { isObjectLiteral } from '@/helpers/commons'
|
||||
import i18n from '@/i18n'
|
||||
import store from '@/store'
|
||||
import type { StateVariant } from '@/types/commons'
|
||||
|
||||
export type RequestStatus = 'pending' | 'success' | 'warning' | 'error'
|
||||
|
@ -47,6 +49,8 @@ export const STATUS_VARIANT = {
|
|||
} as const
|
||||
|
||||
export const useRequests = createGlobalState(() => {
|
||||
const router = useRouter()
|
||||
|
||||
const requests = shallowRef<APIRequest[]>([])
|
||||
const currentRequest = computed(() => {
|
||||
return requests.value.find((r) => r.showModal)
|
||||
|
@ -146,6 +150,41 @@ export const useRequests = createGlobalState(() => {
|
|||
}, 350)
|
||||
}
|
||||
|
||||
function handleAPIError(err: APIError) {
|
||||
err.log()
|
||||
if (err.code === 401) {
|
||||
// Unauthorized
|
||||
store.dispatch('DISCONNECT')
|
||||
} else if (err instanceof APIErrorLog) {
|
||||
// Errors that have produced logs
|
||||
router.push({ name: 'tool-log', params: { name: err.logRef } })
|
||||
} else {
|
||||
const request = requests.value.find((r) => r.id === err.requestId)!
|
||||
request.err = err
|
||||
}
|
||||
}
|
||||
|
||||
function showModal(requestId: APIRequest['id']) {
|
||||
const request = requests.value.find((r) => r.id === requestId)!
|
||||
request.showModal = true
|
||||
}
|
||||
|
||||
function dismissModal(requestId: APIRequest['id']) {
|
||||
const request = requests.value.find((r) => r.id === requestId)!
|
||||
|
||||
if (request.err && request.initial) {
|
||||
// In case of an initial request (data that is needed by a view to render itself),
|
||||
// try to go back so the user doesn't get stuck at a never ending skeleton view.
|
||||
if (history.length > 2) {
|
||||
history.back()
|
||||
} else {
|
||||
// if the url was opened in a new tab, return to home
|
||||
router.push({ name: 'home' })
|
||||
}
|
||||
}
|
||||
request.showModal = false
|
||||
}
|
||||
|
||||
return {
|
||||
requests,
|
||||
historyList,
|
||||
|
@ -153,5 +192,8 @@ export const useRequests = createGlobalState(() => {
|
|||
locked,
|
||||
startRequest,
|
||||
endRequest,
|
||||
handleAPIError,
|
||||
dismissModal,
|
||||
showModal,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { createApp, type Component } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createBootstrap } from 'bootstrap-vue-next'
|
||||
import { VueShowdownPlugin } from 'vue-showdown'
|
||||
import { watchOnce } from '@vueuse/core'
|
||||
import { createBootstrap } from 'bootstrap-vue-next'
|
||||
import { createApp, type Component } from 'vue'
|
||||
import { VueShowdownPlugin } from 'vue-showdown'
|
||||
|
||||
import App from './App.vue'
|
||||
import { APIError } from './api/errors'
|
||||
import { useRequests } from './composables/useRequests'
|
||||
import { useSettings } from './composables/useSettings'
|
||||
import store from './store'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
|
||||
import { registerGlobalErrorHandlers } from './api'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
import '@/scss/main.scss'
|
||||
|
||||
|
@ -17,6 +17,24 @@ type Module = { default: Component }
|
|||
|
||||
const app = createApp(App)
|
||||
|
||||
// Error catching
|
||||
function onError(err: unknown) {
|
||||
if (err instanceof APIError) {
|
||||
useRequests().handleAPIError(err)
|
||||
} else {
|
||||
// FIXME Error modal for internal code error?
|
||||
throw err
|
||||
}
|
||||
}
|
||||
app.config.errorHandler = (err) => onError(err)
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
// Global catching of unhandled promise's rejections.
|
||||
// Those errors (thrown or rejected from inside a promise) can't be catched by
|
||||
// `window.onerror` or vue.
|
||||
e.preventDefault()
|
||||
onError(e.reason)
|
||||
})
|
||||
|
||||
app.use(store)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
@ -46,7 +64,5 @@ Object.values(globalComponentsModules).forEach(
|
|||
},
|
||||
)
|
||||
|
||||
registerGlobalErrorHandlers()
|
||||
|
||||
// Load default locales translations files then mount the app
|
||||
watchOnce(useSettings().localesLoaded, () => app.mount('#app'))
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import { useRequests } from '@/composables/useRequests'
|
||||
import { useSettings } from '@/composables/useSettings'
|
||||
import routes from './routes'
|
||||
import store from '@/store'
|
||||
import routes from './routes'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
|
@ -32,8 +33,10 @@ router.beforeEach((to, from, next) => {
|
|||
updateTransitionName({ to, from })
|
||||
}
|
||||
|
||||
if (store.getters.error) {
|
||||
store.dispatch('DISMISS_ERROR', true)
|
||||
const { currentRequest, dismissModal } = useRequests()
|
||||
if (currentRequest.value?.err) {
|
||||
// In case an error is still present after code route change
|
||||
dismissModal(currentRequest.value.id)
|
||||
}
|
||||
|
||||
if (to.name === 'post-install' && store.getters.installed) {
|
||||
|
|
|
@ -10,7 +10,6 @@ export default {
|
|||
connected: localStorage.getItem('connected') === 'true', // Boolean
|
||||
yunohost: null, // Object { version, repo }
|
||||
reconnecting: null, // null|Object { attemps, delay, initialDelay }
|
||||
error: null, // null || request
|
||||
routerKey: undefined, // String if current route has params
|
||||
breadcrumb: [], // Array of routes
|
||||
transitionName: null, // String of CSS class if transitions are enabled
|
||||
|
@ -34,14 +33,6 @@ export default {
|
|||
state.reconnecting = args
|
||||
},
|
||||
|
||||
SET_ERROR(state, request) {
|
||||
if (request) {
|
||||
state.error = request
|
||||
} else {
|
||||
state.error = null
|
||||
}
|
||||
},
|
||||
|
||||
SET_ROUTER_KEY(state, key) {
|
||||
state.routerKey = key
|
||||
},
|
||||
|
@ -134,48 +125,6 @@ export default {
|
|||
})
|
||||
},
|
||||
|
||||
HANDLE_ERROR({ commit, dispatch }, error) {
|
||||
if (error.code === 401) {
|
||||
// Unauthorized
|
||||
dispatch('DISCONNECT')
|
||||
} else if (error.logRef) {
|
||||
// Errors that have produced logs
|
||||
router.push({ name: 'tool-log', params: { name: error.logRef } })
|
||||
} else {
|
||||
// The request is temporarely stored in the error for reference, but we reverse
|
||||
// the ownership to stay generic.
|
||||
const request = error.request
|
||||
delete error.request
|
||||
request.error = error
|
||||
// Display the error in a modal on the current view.
|
||||
commit('SET_ERROR', request)
|
||||
}
|
||||
},
|
||||
|
||||
REVIEW_ERROR({ commit }, request) {
|
||||
request.review = true
|
||||
commit('SET_ERROR', request)
|
||||
},
|
||||
|
||||
DISMISS_ERROR({ commit, state }, { initial, review = false }) {
|
||||
if (initial && !review) {
|
||||
// In case of an initial request (data that is needed by a view to render itself),
|
||||
// try to go back so the user doesn't get stuck at a never ending skeleton view.
|
||||
if (history.length > 2) {
|
||||
history.back()
|
||||
} else {
|
||||
// if the url was opened in a new tab, return to home
|
||||
router.push({ name: 'home' })
|
||||
}
|
||||
}
|
||||
commit('SET_ERROR', null)
|
||||
},
|
||||
|
||||
DISMISS_WARNING({ commit, state }, request) {
|
||||
commit('SET_WAITING', false)
|
||||
delete request.showWarningMessage
|
||||
},
|
||||
|
||||
UPDATE_ROUTER_KEY({ commit }, { to, from }) {
|
||||
if (isEmptyValue(to.params)) {
|
||||
commit('SET_ROUTER_KEY', undefined)
|
||||
|
@ -257,11 +206,8 @@ export default {
|
|||
installed: (state) => state.installed,
|
||||
connected: (state) => state.connected,
|
||||
yunohost: (state) => state.yunohost,
|
||||
error: (state) => state.error,
|
||||
reconnecting: (state) => state.reconnecting,
|
||||
history: (state) => state.history,
|
||||
lastAction: (state) => state.history[state.history.length - 1],
|
||||
currentRequest: (state) => state.currentRequest,
|
||||
routerKey: (state) => state.routerKey,
|
||||
breadcrumb: (state) => state.breadcrumb,
|
||||
transitionName: (state) => state.transitionName,
|
||||
|
|
Loading…
Add table
Reference in a new issue