Fix playlist modal error handling

This commit is contained in:
wvffle 2022-07-01 11:13:07 +00:00 committed by Georg Krause
parent 23a88d025a
commit 03e29b3fbc
4 changed files with 89 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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