Fix playlist modal error handling
This commit is contained in:
parent
23a88d025a
commit
03e29b3fbc
|
@ -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
|
||||||
|
|
|
@ -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,56 +30,71 @@ 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) {
|
||||||
error.backendErrors.push('Resource not found')
|
case 404:
|
||||||
const message = error.response?.data
|
error.backendErrors.push('Resource not found')
|
||||||
store.commit('ui/addMessage', {
|
store.commit('ui/addMessage', {
|
||||||
content: message,
|
content: error.response?.data,
|
||||||
class: 'error'
|
class: 'error'
|
||||||
})
|
})
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
error.backendErrors.push('Permission denied')
|
case 403:
|
||||||
} else if (error.response?.status === 429) {
|
error.backendErrors.push('Permission denied')
|
||||||
let message
|
break
|
||||||
const rateLimitStatus: RateLimitStatus = {
|
|
||||||
limit: error.response?.headers['x-ratelimit-limit'],
|
case 429: {
|
||||||
scope: error.response?.headers['x-ratelimit-scope'],
|
let message
|
||||||
remaining: error.response?.headers['x-ratelimit-remaining'],
|
const rateLimitStatus: RateLimitStatus = {
|
||||||
duration: error.response?.headers['x-ratelimit-duration'],
|
limit: error.response?.headers['x-ratelimit-limit'],
|
||||||
availableSeconds: parseInt(error.response?.headers['retry-after'] ?? 60),
|
scope: error.response?.headers['x-ratelimit-scope'],
|
||||||
reset: error.response?.headers['x-ratelimit-reset'],
|
remaining: error.response?.headers['x-ratelimit-remaining'],
|
||||||
resetSeconds: error.response?.headers['x-ratelimit-resetseconds']
|
duration: error.response?.headers['x-ratelimit-duration'],
|
||||||
}
|
availableSeconds: parseInt(error.response?.headers['retry-after'] ?? 60),
|
||||||
if (rateLimitStatus.availableSeconds) {
|
reset: error.response?.headers['x-ratelimit-reset'],
|
||||||
const tryAgain = moment().add(rateLimitStatus.availableSeconds, 's').toNow(true)
|
resetSeconds: error.response?.headers['x-ratelimit-resetseconds']
|
||||||
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 {
|
if (rateLimitStatus.availableSeconds) {
|
||||||
message = $pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again later')
|
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 }')
|
||||||
error.backendErrors.push(message)
|
message = $gettext(message, { delay: tryAgain })
|
||||||
store.commit('ui/addMessage', {
|
} else {
|
||||||
content: message,
|
message = $pgettext('*/Error/Paragraph', 'You sent too many requests and have been rate limited, please try again later')
|
||||||
date: new Date(),
|
}
|
||||||
class: 'error'
|
|
||||||
})
|
error.backendErrors.push(message)
|
||||||
logger.error('This client is rate-limited!', rateLimitStatus)
|
store.commit('ui/addMessage', {
|
||||||
} else if (error.response?.status === 500) {
|
content: message,
|
||||||
error.backendErrors.push('A server error occurred')
|
date: new Date(),
|
||||||
} else if (error.response?.data) {
|
class: 'error'
|
||||||
if (error.response?.data.detail) {
|
})
|
||||||
error.backendErrors.push(error.response.data.detail)
|
|
||||||
} else {
|
logger.error('This client is rate-limited!', rateLimitStatus)
|
||||||
error.rawPayload = error.response.data
|
break
|
||||||
const parsedErrors = parseAPIErrors(error.response.data)
|
|
||||||
error.backendErrors = [...error.backendErrors, ...parsedErrors]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (error.backendErrors.length === 0) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)) {
|
errors.push(...value.map(err => {
|
||||||
const values = value
|
if (typeof err === 'string') {
|
||||||
errors.push(...values.map(err => {
|
|
||||||
return err.toLocaleLowerCase().includes('this field ')
|
return err.toLocaleLowerCase().includes('this field ')
|
||||||
? `${fieldName}: ${err}`
|
? `${fieldName}: ${err}`
|
||||||
: err
|
: err
|
||||||
}))
|
}
|
||||||
} else if (value) {
|
|
||||||
// nested errors
|
return startCase(err.code.replace(/_/g, ' '))
|
||||||
const nestedErrors = parseAPIErrors(value, fieldName)
|
}))
|
||||||
errors.push(...nestedErrors)
|
|
||||||
}
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle nested errors
|
||||||
|
errors.push(...parseAPIErrors(value, fieldName))
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
Loading…
Reference in New Issue