Commit 180ba939 authored by Konrad Mohrfeldt's avatar Konrad Mohrfeldt
Browse files

feat: allow API store consumers to use traditional flow-control

The currently prevalent API response handling is based on a callback
pattern. This has at least two major drawbacks:

1. More broadly it facilitates the use of nested callbacks, which make
   the code harder to read and clutter stack traces.
2. Our specific callback solution does not follow the traditional node
   callback pattern that looks like `function (err, data) { ... }`.
   Instead we have `callback` and `callbackCancel`, none of which are
   meant to handle actual error/exception objects.

The latter makes it hard to write code that is executed irrespective of
the specific code path, like in a try-finally clause. In practice it’s
also a violation of the separation-of-concerns design principle as it
forces error handling to happen in the store function instead of the
caller that is best suited to handle error states.

This change attempts to facilitate a gradual migration to a
Promise-based result handling by it to co-exist with the currently used
callback pattern. Callers that don’t provide any callback functions are
assumed to handle promises whereas callers that do provide callback
functions will see no change in behaviour.

This allows us to transition one API-call at a time instead of doing one
large and time-consuming refactoring.

refs #55
parent d82c8719
Pipeline #1846 passed with stage
in 1 minute and 32 seconds
import { has } from '@/utilities'
function isErrorObject (obj) {
return has(obj, 'message') && has(obj, 'code')
}
/**
* Parses the variety of error formats that are emitted by
* Django REST framework and yields error objects.
*/
function * extractResponseErrors(responseData) {
if (isErrorObject(responseData)) {
yield responseData
} else if (has(responseData, 'detail') && isErrorObject(responseData.detail)) {
yield responseData.detail
} else if (Array.isArray(responseData)) {
for (const item of responseData) {
if (isErrorObject(item)) {
yield item
}
}
}
}
export class APIError extends Error {
constructor(message, response) {
super(message ?? response?.statusText)
this.response = response
this.errors = Array.from(extractResponseErrors(response?.data))
}
/**
* Handle API response errors by either calling the provided callbacks
* or throwing exceptions that can be processed in promise handlers.
*/
static handle(error, message, context, vm) {
if (typeof context?.callback === 'function' || typeof context?.callbackCancel === 'function') {
// This API request was made by a callback-style caller,
// so we do error handling like we used to do it.
handleApiError(vm, error, message)
context?.callbackCancel?.()
} else {
// This API request was made by a caller that did not define any callback handlers.
// We take this as an indication, that the caller wants to use
// traditional flow-control with try-catch statements.
throw new APIError(message, error.response)
}
}
}
export function handleApiError(vm, err, message) {
if (err.response) {
vm.$log.error(err.response.status + ' ' + err.response.statusText)
vm.$log.error(err.response)
let msg = 'Error: '
if (message) {
msg += message
}
msg += '\n\n'
msg += 'Status code: ' + err.response.status + ' ' + err.response.statusText
alert(msg)
} else {
vm.$log.error(err)
if (message) {
alert('Error: ' + message + '\nInspect the console for more details.')
} else {
alert('Unspecified error. Inspect the console for details.')
}
}
}
/**
* Like APIError.handle this is a helper to allow us to migrate from the
* callback-oriented to promise-based response handling.
*
* When a callback is provided it mimics the old callback behaviour and
* calls the callback with the response and returns undefined.
* If no callback is provided we assume the caller wants to handle
* the response promise.
*/
export function callOrReturn (response, callback) {
if (typeof callback === 'function') {
callback(response)
return undefined
} else {
return response
}
}
export default function handleApiError(vm, err, message) {
if (err.response) {
vm.$log.error(err.response.status + ' ' + err.response.statusText)
vm.$log.error(err.response)
let msg = 'Error: '
if (message) {
msg += message
}
msg += '\n\n'
msg += 'Status code: ' + err.response.status + ' ' + err.response.statusText
alert(msg)
} else {
vm.$log.error(err)
if (message) {
alert('Error: ' + message + '\nInspect the console for more details.')
} else {
alert('Unspecified error. Inspect the console for details.')
}
}
}
import axios from 'axios' import axios from 'axios'
import oidc from 'oidc-client' import oidc from 'oidc-client'
import handleApiError from '../handleApiError' import { handleApiError } from '../api-helper'
const oidcmgr = new oidc.UserManager({ const oidcmgr = new oidc.UserManager({
userStore: new oidc.WebStorageStateStore(), userStore: new oidc.WebStorageStateStore(),
......
import axios from 'axios' import axios from 'axios'
import handleApiError from '../handleApiError' import { handleApiError } from '../api-helper'
const state = { const state = {
files: [], files: [],
......
import axios from 'axios' import axios from 'axios'
import handleApiError from '../handleApiError' import { handleApiError } from '../api-helper'
const state = { const state = {
playlists: [], playlists: [],
......
import axios from 'axios' import axios from 'axios'
import handleApiError from '../handleApiError' import { handleApiError } from '../api-helper'
const cloneMinimalShowObject = function (show) { const cloneMinimalShowObject = function (show) {
/* returns a new minimal object from the current show object with all /* returns a new minimal object from the current show object with all
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment