Skip to content

Commit

Permalink
chore: graceful failure from extend/override errors (#4134)
Browse files Browse the repository at this point in the history
  • Loading branch information
SychO9 authored Dec 7, 2024
1 parent 27087cc commit 5d281b9
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 11 deletions.
29 changes: 23 additions & 6 deletions framework/core/js/src/common/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,13 @@ export default class Application {

initialRoute!: string;

/**
* @internal
*/
public currentInitializerExtension: string | null = null;

private handledErrors: { extension: null | string; errorId: string; error: any }[] = [];

public load(payload: Application['data']) {
this.data = payload;
this.translator.setLocale(payload.locale);
Expand All @@ -288,17 +295,19 @@ export default class Application {
const caughtInitializationErrors: CallableFunction[] = [];

this.initializers.toArray().forEach((initializer) => {
this.currentInitializerExtension = initializer.itemName.includes('/')
? initializer.itemName.replace(/(\/flarum-ext-)|(\/flarum-)/g, '-')
: initializer.itemName;

try {
initializer(this);
} catch (e) {
const extension = initializer.itemName.includes('/')
? initializer.itemName.replace(/(\/flarum-ext-)|(\/flarum-)/g, '-')
: initializer.itemName;

caughtInitializationErrors.push(() =>
fireApplicationError(
extractText(app.translator.trans('core.lib.error.extension_initialiation_failed_message', { extension })),
`${extension} failed to initialize`,
extractText(
app.translator.trans('core.lib.error.extension_initialiation_failed_message', { extension: this.currentInitializerExtension })
),
`${this.currentInitializerExtension} failed to initialize`,
e
)
);
Expand Down Expand Up @@ -727,4 +736,12 @@ export default class Application {

return prefix + url + (queryString ? '?' + queryString : '');
}

public handleErrorOnce(extension: null | string, errorId: string, userTitle: string, consoleTitle: string, error: any) {
if (this.handledErrors.some((e) => e.errorId === errorId)) return;

this.handledErrors.push({ extension, errorId, error });

fireApplicationError(userTitle, consoleTitle, error);
}
}
31 changes: 29 additions & 2 deletions framework/core/js/src/common/extend.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import extractText from './utils/extractText';
import app from './app';

/**
* Extend an object's method by running its output through a mutating callback
* every time it is called.
Expand Down Expand Up @@ -28,6 +31,8 @@ export function extend<T extends Record<string, any>, K extends KeyOfType<T, Fun
methods: K | K[],
callback: (this: T, val: ReturnType<T[K]>, ...args: Parameters<T[K]>) => void
) {
const extension = app.currentInitializerExtension;

// A lazy loaded module, only apply the function after the module is loaded.
if (typeof object === 'string') {
let [namespace, id] = flarum.reg.namespaceAndIdFromPath(object);
Expand All @@ -45,7 +50,17 @@ export function extend<T extends Record<string, any>, K extends KeyOfType<T, Fun
object[method] = function (this: T, ...args: Parameters<T[K]>) {
const value = original ? original.apply(this, args) : undefined;

callback.apply(this, [value, ...args]);
try {
callback.apply(this, [value, ...args]);
} catch (e) {
app.handleErrorOnce(
extension,
`${extension}::extend::${object.constructor.name}::${method.toString()}`,
extractText(app.translator.trans('core.lib.error.extension_runtime_failed_message', { extension })),
`${extension} failed to extend ${object.constructor.name}::${method.toString()}`,
e
);
}

return value;
} as T[K];
Expand Down Expand Up @@ -86,6 +101,8 @@ export function override<T extends Record<any, any>, K extends KeyOfType<T, Func
methods: K | K[],
newMethod: (this: T, orig: T[K], ...args: Parameters<T[K]>) => void
) {
const extension = app.currentInitializerExtension;

// A lazy loaded module, only apply the function after the module is loaded.
if (typeof object === 'string') {
let [namespace, id] = flarum.reg.namespaceAndIdFromPath(object);
Expand All @@ -101,7 +118,17 @@ export function override<T extends Record<any, any>, K extends KeyOfType<T, Func
const original: Function = object[method];

object[method] = function (this: T, ...args: Parameters<T[K]>) {
return newMethod.apply(this, [original?.bind(this), ...args]);
try {
return newMethod.apply(this, [original?.bind(this), ...args]);
} catch (e) {
app.handleErrorOnce(
extension,
`${extension}::extend::${object.constructor.name}::${method.toString()}`,
extractText(app.translator.trans('core.lib.error.extension_runtime_failed_message', { extension })),
`${extension} failed to override ${object.constructor.name}::${method.toString()}`,
e
);
}
} as T[K];

Object.assign(object[method], original);
Expand Down
6 changes: 3 additions & 3 deletions framework/core/js/src/common/helpers/fireApplicationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import app from '../app';
/**
* Fire a Flarum error which is shown in the JS console for everyone and in an alert for the admin.
*
* @param userTitle: a user friendly title of the error, should be localized.
* @param consoleTitle: an error title that goes in the console, doesn't have to be localized.
* @param error: the error.
* @param userTitle a user friendly title of the error, should be localized.
* @param consoleTitle an error title that goes in the console, doesn't have to be localized.
* @param error the error.
*/
export default function fireApplicationError(userTitle: string, consoleTitle: string, error: any) {
console.group(`%c${consoleTitle}`, 'background-color: #d83e3e; color: #ffffff; font-weight: bold;');
Expand Down
1 change: 1 addition & 0 deletions framework/core/locale/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ core:
db_error_message: "Database query failed. This may be caused by an incompatibility between an extension and your database driver."
dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}"
extension_initialiation_failed_message: "{extension} failed to initialize, check the browser console for further information."
extension_runtime_failed_message: "{extension} encountered an error while running. Check the browser console for further information."
generic_message: "Oops! Something went wrong. Please reload the page and try again."
generic_cross_origin_message: "Oops! Something went wrong during a cross-origin request. Please reload the page and try again."
missing_dependencies_message: "Cannot enable {extension} until the following dependencies are enabled: {extensions}"
Expand Down

0 comments on commit 5d281b9

Please sign in to comment.