mirror of
https://github.com/flarum/framework.git
synced 2025-06-06 16:24:33 +08:00
Split up application error handling (#3184)
Co-authored-by: David Wheatley <hi@davwheat.dev>
This commit is contained in:

committed by
GitHub

parent
326b787130
commit
57b413ada5
@ -366,7 +366,7 @@ export default class Application {
|
||||
}
|
||||
|
||||
protected transformRequestOptions<ResponseType>(flarumOptions: FlarumRequestOptions<ResponseType>): InternalFlarumRequestOptions<ResponseType> {
|
||||
const { background, deserialize, errorHandler, extract, modifyText, ...tmpOptions } = { ...flarumOptions };
|
||||
const { background, deserialize, extract, modifyText, ...tmpOptions } = { ...flarumOptions };
|
||||
|
||||
// Unless specified otherwise, requests should run asynchronously in the
|
||||
// background, so that they don't prevent redraws from occurring.
|
||||
@ -380,10 +380,6 @@ export default class Application {
|
||||
// so it errors due to Mithril's typings
|
||||
const defaultDeserialize = (response: string) => response as ResponseType;
|
||||
|
||||
const defaultErrorHandler = (error: RequestError) => {
|
||||
throw error;
|
||||
};
|
||||
|
||||
// When extracting the data from the response, we can check the server
|
||||
// response code and show an error message to the user if something's gone
|
||||
// awry.
|
||||
@ -392,7 +388,6 @@ export default class Application {
|
||||
const options: InternalFlarumRequestOptions<ResponseType> = {
|
||||
background: background ?? defaultBackground,
|
||||
deserialize: deserialize ?? defaultDeserialize,
|
||||
errorHandler: errorHandler ?? defaultErrorHandler,
|
||||
...tmpOptions,
|
||||
};
|
||||
|
||||
@ -460,86 +455,90 @@ export default class Application {
|
||||
|
||||
if (this.requestErrorAlert) this.alerts.dismiss(this.requestErrorAlert);
|
||||
|
||||
// Now make the request. If it's a failure, inspect the error that was
|
||||
// returned and show an alert containing its contents.
|
||||
return m.request(options).then(
|
||||
(response) => response,
|
||||
(error: RequestError) => {
|
||||
let content;
|
||||
|
||||
switch (error.status) {
|
||||
case 422:
|
||||
content = ((error.response?.errors ?? {}) as Record<string, unknown>[])
|
||||
.map((error) => [error.detail, <br />])
|
||||
.flat()
|
||||
.slice(0, -1);
|
||||
break;
|
||||
|
||||
case 401:
|
||||
case 403:
|
||||
content = app.translator.trans('core.lib.error.permission_denied_message');
|
||||
break;
|
||||
|
||||
case 404:
|
||||
case 410:
|
||||
content = app.translator.trans('core.lib.error.not_found_message');
|
||||
break;
|
||||
|
||||
case 413:
|
||||
content = app.translator.trans('core.lib.error.payload_too_large_message');
|
||||
break;
|
||||
|
||||
case 429:
|
||||
content = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
||||
break;
|
||||
|
||||
default:
|
||||
content = app.translator.trans('core.lib.error.generic_message');
|
||||
}
|
||||
|
||||
const isDebug = app.forum.attribute('debug');
|
||||
// contains a formatted errors if possible, response must be an JSON API array of errors
|
||||
// the details property is decoded to transform escaped characters such as '\n'
|
||||
const errors = error.response && error.response.errors;
|
||||
const formattedError = (Array.isArray(errors) && errors?.[0]?.detail && errors.map((e) => decodeURI(e.detail ?? ''))) || undefined;
|
||||
|
||||
error.alert = {
|
||||
type: 'error',
|
||||
content,
|
||||
controls: isDebug && [
|
||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error, formattedError)}>
|
||||
{app.translator.trans('core.lib.debug_button')}
|
||||
</Button>,
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
options.errorHandler(error);
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError) {
|
||||
if (isDebug && error.xhr) {
|
||||
const { method, url } = error.options;
|
||||
const { status = '' } = error.xhr;
|
||||
|
||||
console.group(`${method} ${url} ${status}`);
|
||||
|
||||
console.error(...(formattedError || [error]));
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
this.requestErrorAlert = this.alerts.show(error.alert, error.alert.content);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
return m.request(options).catch((e) => this.requestErrorCatch(e, originalOptions.errorHandler));
|
||||
}
|
||||
|
||||
private showDebug(error: RequestError, formattedError?: string[]) {
|
||||
/**
|
||||
* By default, show an error alert, and log the error to the console.
|
||||
*/
|
||||
protected requestErrorCatch<ResponseType>(error: RequestError, customErrorHandler: FlarumRequestOptions<ResponseType>['errorHandler']) {
|
||||
// the details property is decoded to transform escaped characters such as '\n'
|
||||
const formattedErrors = error.response?.errors?.map((e) => decodeURI(e.detail ?? '')) ?? [];
|
||||
|
||||
let content;
|
||||
switch (error.status) {
|
||||
case 422:
|
||||
content = formattedErrors
|
||||
.map((detail) => [detail, <br />])
|
||||
.flat()
|
||||
.slice(0, -1);
|
||||
break;
|
||||
|
||||
case 401:
|
||||
case 403:
|
||||
content = app.translator.trans('core.lib.error.permission_denied_message');
|
||||
break;
|
||||
|
||||
case 404:
|
||||
case 410:
|
||||
content = app.translator.trans('core.lib.error.not_found_message');
|
||||
break;
|
||||
|
||||
case 413:
|
||||
content = app.translator.trans('core.lib.error.payload_too_large_message');
|
||||
break;
|
||||
|
||||
case 429:
|
||||
content = app.translator.trans('core.lib.error.rate_limit_exceeded_message');
|
||||
break;
|
||||
|
||||
default:
|
||||
content = app.translator.trans('core.lib.error.generic_message');
|
||||
}
|
||||
|
||||
const isDebug: boolean = app.forum.attribute('debug');
|
||||
|
||||
error.alert = {
|
||||
type: 'error',
|
||||
content,
|
||||
controls: isDebug && [
|
||||
<Button className="Button Button--link" onclick={this.showDebug.bind(this, error, formattedErrors)}>
|
||||
{app.translator.trans('core.lib.debug_button')}
|
||||
</Button>,
|
||||
],
|
||||
};
|
||||
|
||||
if (customErrorHandler) {
|
||||
customErrorHandler(error);
|
||||
} else {
|
||||
this.requestErrorDefaultHandler(error, isDebug, formattedErrors);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
protected requestErrorDefaultHandler(e: unknown, isDebug: boolean, formattedErrors: string[]): void {
|
||||
if (e instanceof RequestError) {
|
||||
if (isDebug && e.xhr) {
|
||||
const { method, url } = e.options;
|
||||
const { status = '' } = e.xhr;
|
||||
|
||||
console.group(`${method} ${url} ${status}`);
|
||||
|
||||
console.error(...(formattedErrors || [e]));
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
if (e.alert) {
|
||||
this.requestErrorAlert = this.alerts.show(e.alert, e.alert.content);
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private showDebug(error: RequestError, formattedError: string[]) {
|
||||
if (this.requestErrorAlert !== null) this.alerts.dismiss(this.requestErrorAlert);
|
||||
|
||||
this.modal.show(RequestErrorModal, { error, formattedError });
|
||||
|
@ -1,7 +1,7 @@
|
||||
import type Mithril from 'mithril';
|
||||
import type { AlertAttrs } from '../components/Alert';
|
||||
|
||||
export type InternalFlarumRequestOptions<ResponseType> = Mithril.RequestOptions<ResponseType> & {
|
||||
errorHandler: (error: RequestError) => void;
|
||||
url: string;
|
||||
};
|
||||
|
||||
@ -20,7 +20,7 @@ export default class RequestError<ResponseType = string> {
|
||||
}[];
|
||||
} | null;
|
||||
|
||||
alert: any;
|
||||
alert: AlertAttrs | null;
|
||||
|
||||
constructor(status: number, responseText: string | null, options: InternalFlarumRequestOptions<ResponseType>, xhr: XMLHttpRequest) {
|
||||
this.status = status;
|
||||
|
Reference in New Issue
Block a user