Fix playlist modal error handling

environments/review-front-deve-otr6gc/deployments/13419
wvffle 2022-07-01 11:13:07 +00:00 zatwierdzone przez Georg Krause
rodzic 23a88d025a
commit 03e29b3fbc
4 zmienionych plików z 89 dodań i 73 usunięć

Wyświetl plik

@ -1,7 +1,7 @@
<script setup lang="ts">
import { filter, sortBy, flow } from 'lodash-es'
import axios, { AxiosError } from 'axios'
import axios from 'axios'
import { useGettext } from 'vue3-gettext'
import Modal from '~/components/semantic/Modal.vue'
@ -9,7 +9,7 @@ import PlaylistForm from '~/components/playlists/Form.vue'
import useLogger from '~/composables/useLogger'
import { useStore } from '~/store'
import { ref, computed, watch } from 'vue'
import { BackendError, Playlist } from '~/types'
import { BackendError, Playlist, APIErrorResponse } from '~/types'
import { useRouter } from 'vue-router'
const logger = useLogger()
@ -46,7 +46,7 @@ watch(() => store.state.playlists.showModal, () => {
})
const lastSelectedPlaylist = ref(-1)
const errors = ref([] as AxiosError[])
const errors = ref([] as string[])
const duplicateTrackAddInfo = ref({} as { playlist_name?: string })
const addToPlaylist = async (playlistId: number, allowDuplicates: boolean) => {
@ -63,10 +63,12 @@ const addToPlaylist = async (playlistId: number, allowDuplicates: boolean) => {
store.dispatch('playlists/fetchOwn')
} catch (error) {
if (error as BackendError) {
const { backendErrors } = error as BackendError
const { backendErrors, rawPayload = {} } = error as BackendError
if (backendErrors.length === 1 && backendErrors[0].code === 'tracks_already_exist_in_playlist') {
duplicateTrackAddInfo.value = backendErrors[0] as unknown as { playlist_name: string }
// TODO (wvffle): Test if it works
// if (backendErrors.length === 1 && backendErrors[0].code === 'tracks_already_exist_in_playlist') {
if (backendErrors.length === 1 && backendErrors[0] === 'Tracks already exist in playlist') {
duplicateTrackAddInfo.value = ((rawPayload.playlist as APIErrorResponse).non_field_errors as APIErrorResponse)[0] as object
showDuplicateTrackAddConfirmation.value = true
} else {
errors.value = backendErrors

Wyświetl plik

@ -1,4 +1,4 @@
import { BackendError, InitModule, RateLimitStatus } from '~/types'
import { APIErrorResponse, BackendError, InitModule, RateLimitStatus } from '~/types'
import createAuthRefreshInterceptor from 'axios-auth-refresh'
import axios, { AxiosError } from 'axios'
@ -30,56 +30,71 @@ export const install: InitModule = ({ store, router }) => {
return response
}, async (error: BackendError) => {
error.backendErrors = []
if (store.state.auth.authenticated && !store.state.auth.oauth.accessToken && error.response?.status === 401) {
store.commit('auth/authenticated', false)
logger.warn('Received 401 response from API, redirecting to login form', router.currentRoute.value.fullPath)
await router.push({ name: 'login', query: { next: router.currentRoute.value.fullPath } })
}
if (error.response?.status === 404) {
error.backendErrors.push('Resource not found')
const message = error.response?.data
store.commit('ui/addMessage', {
content: message,
class: 'error'
})
} else if (error.response?.status === 403) {
error.backendErrors.push('Permission denied')
} else if (error.response?.status === 429) {
let message
const rateLimitStatus: RateLimitStatus = {
limit: error.response?.headers['x-ratelimit-limit'],
scope: error.response?.headers['x-ratelimit-scope'],
remaining: error.response?.headers['x-ratelimit-remaining'],
duration: error.response?.headers['x-ratelimit-duration'],
availableSeconds: parseInt(error.response?.headers['retry-after'] ?? 60),
reset: error.response?.headers['x-ratelimit-reset'],
resetSeconds: error.response?.headers['x-ratelimit-resetseconds']
}
if (rateLimitStatus.availableSeconds) {
const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
message = $pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again in %{ delay }')
message = $gettext(message, { delay: tryAgain })
} else {
message = $pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again later')
}
error.backendErrors.push(message)
store.commit('ui/addMessage', {
content: message,
date: new Date(),
class: 'error'
})
logger.error('This client is rate-limited!', rateLimitStatus)
} else if (error.response?.status === 500) {
error.backendErrors.push('A server error occurred')
} else if (error.response?.data) {
if (error.response?.data.detail) {
error.backendErrors.push(error.response.data.detail)
} else {
error.rawPayload = error.response.data
const parsedErrors = parseAPIErrors(error.response.data)
error.backendErrors = [...error.backendErrors, ...parsedErrors]
switch (error.response?.status) {
case 404:
error.backendErrors.push('Resource not found')
store.commit('ui/addMessage', {
content: error.response?.data,
class: 'error'
})
case 403:
error.backendErrors.push('Permission denied')
break
case 429: {
let message
const rateLimitStatus: RateLimitStatus = {
limit: error.response?.headers['x-ratelimit-limit'],
scope: error.response?.headers['x-ratelimit-scope'],
remaining: error.response?.headers['x-ratelimit-remaining'],
duration: error.response?.headers['x-ratelimit-duration'],
availableSeconds: parseInt(error.response?.headers['retry-after'] ?? 60),
reset: error.response?.headers['x-ratelimit-reset'],
resetSeconds: error.response?.headers['x-ratelimit-resetseconds']
}
if (rateLimitStatus.availableSeconds) {
const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
message = $pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again in %{ delay }')
message = $gettext(message, { delay: tryAgain })
} else {
message = $pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again later')
}
error.backendErrors.push(message)
store.commit('ui/addMessage', {
content: message,
date: new Date(),
class: 'error'
})
logger.error('This client is rate-limited!', rateLimitStatus)
break
}
case 500:
error.backendErrors.push('A server error occurred')
break
default:
if (error.response?.data as object) {
const data = error.response?.data as Record<string, unknown>
if (data?.detail) {
error.backendErrors.push(data.detail as string)
} else {
error.rawPayload = data as APIErrorResponse
const parsedErrors = parseAPIErrors(data as APIErrorResponse)
error.backendErrors = [...error.backendErrors, ...parsedErrors]
}
}
}
if (error.backendErrors.length === 0) {

Wyświetl plik

@ -94,13 +94,11 @@ export interface Playlist {
}
// API stuff
export interface APIErrorResponse {
[key: string]: APIErrorResponse | string[]
}
export interface APIErrorResponse extends Record<string, APIErrorResponse | string[] | { code: string }[]> {}
export interface BackendError {
backendErrors: AxiosError[]
rawPayload?: object
export interface BackendError extends AxiosError {
backendErrors: string[]
rawPayload?: APIErrorResponse
}
export interface RateLimitStatus {

Wyświetl plik

@ -11,27 +11,28 @@ export function setUpdate (obj: object, statuses: Record<string, unknown>, value
export function parseAPIErrors (responseData: APIErrorResponse, parentField?: string): string[] {
const errors = []
for (const field in responseData) {
if (Object.prototype.hasOwnProperty.call(responseData, field)) {
let fieldName = startCase(field.replace('_', ' '))
if (parentField) {
fieldName = `${parentField} - ${fieldName}`
}
for (const [field, value] of Object.entries(responseData)) {
let fieldName = startCase(field.replace(/_/g, ' '))
if (parentField) {
fieldName = `${parentField} - ${fieldName}`
}
const value = responseData[field]
if (Array.isArray(value)) {
const values = value
errors.push(...values.map(err => {
if (Array.isArray(value)) {
errors.push(...value.map(err => {
if (typeof err === 'string') {
return err.toLocaleLowerCase().includes('this field ')
? `${fieldName}: ${err}`
: err
}))
} else if (value) {
// nested errors
const nestedErrors = parseAPIErrors(value, fieldName)
errors.push(...nestedErrors)
}
}
}
return startCase(err.code.replace(/_/g, ' '))
}))
continue
}
// Handle nested errors
errors.push(...parseAPIErrors(value, fieldName))
}
return errors