From 6c3f782728f9ca3748bf91037c7508f355542e4b Mon Sep 17 00:00:00 2001 From: Segev Finer Date: Mon, 10 Aug 2026 11:22:10 +0300 Subject: [PATCH] Make thenable plugins have the right name on errors When an plugin registerd via an `import` without await timeouts you got the useless `[object Promise]` name in the error message. This PR fixes that by setting name of the plugin earlier so that it is used in the error message. --- index.js | 2 +- lib/plugin.js | 13 +++++++++++ test/fixtures/esm-named.mjs | 3 +++ test/fixtures/esm-no-next.mjs | 3 +++ test/fixtures/esm-plugin-meta.mjs | 8 +++++++ test/plugin-name.test.js | 36 +++++++++++++++++++++++++++++++ test/plugin-timeout.test.js | 15 +++++++++++++ 7 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/esm-named.mjs create mode 100644 test/fixtures/esm-no-next.mjs create mode 100644 test/fixtures/esm-plugin-meta.mjs diff --git a/index.js b/index.js index 64449b31..ef70b39b 100644 --- a/index.js +++ b/index.js @@ -414,7 +414,7 @@ Boot.prototype._loadPlugin = function (plugin, callback) { if (typeof fn.default === 'function') { fn = fn.default } - plugin.func = fn + plugin.setFunc(fn) this._loadPlugin(plugin, callback) }, callback) return diff --git a/lib/plugin.js b/lib/plugin.js index a3f248d5..e4f8d10e 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -56,6 +56,19 @@ function Plugin (queue, func, options, isAfter, timeout) { inherits(Plugin, EventEmitter) +/** + * Replaces the plugin function, keeping the derived name in sync. + * + * Used when a thenable plugin resolves to the actual plugin function, as the + * name derived from the thenable itself is meaningless. + * + * @param {function} func + */ +Plugin.prototype.setFunc = function (func) { + this.func = func + this.name = getPluginName(func, this.options) +} + /** * @callback ExecCallback * @param {Error|null} execErr diff --git a/test/fixtures/esm-named.mjs b/test/fixtures/esm-named.mjs new file mode 100644 index 00000000..3e00834a --- /dev/null +++ b/test/fixtures/esm-named.mjs @@ -0,0 +1,3 @@ +export default async function esmNamedPlugin (app) { + app.loaded = true +} diff --git a/test/fixtures/esm-no-next.mjs b/test/fixtures/esm-no-next.mjs new file mode 100644 index 00000000..77523755 --- /dev/null +++ b/test/fixtures/esm-no-next.mjs @@ -0,0 +1,3 @@ +export default function esmPluginNoNext (app, opts, next) { + // do not call next on purpose +} diff --git a/test/fixtures/esm-plugin-meta.mjs b/test/fixtures/esm-plugin-meta.mjs new file mode 100644 index 00000000..31279e49 --- /dev/null +++ b/test/fixtures/esm-plugin-meta.mjs @@ -0,0 +1,8 @@ +async function plugin (app) { + app.loaded = true +} + +// this symbol is assigned by fastify-plugin +plugin[Symbol.for('plugin-meta')] = { name: 'esm-plugin-meta' } + +export default plugin diff --git a/test/plugin-name.test.js b/test/plugin-name.test.js index f5038196..193a5b4a 100644 --- a/test/plugin-name.test.js +++ b/test/plugin-name.test.js @@ -56,3 +56,39 @@ test('plugins get a name from the function source if theres no other option', as t.assert.strictEqual(jsonToCompare.label, 'root') t.assert.strictEqual(jsonToCompare.nodes[0].label, '(app, opts, next) => next()') }) + +test('thenable plugins get a name from the resolved plugin metadata if it is set', async (t) => { + t.plan(2) + const app = boot() + + app.use(import('./fixtures/esm-plugin-meta.mjs')) + await app.ready() + + const jsonToCompare = app.toJSON() + t.assert.strictEqual(jsonToCompare.label, 'root') + t.assert.strictEqual(jsonToCompare.nodes[0].label, 'esm-plugin-meta') +}) + +test('thenable plugins get a name from the options if theres no metadata', async (t) => { + t.plan(2) + const app = boot() + + app.use(import('./fixtures/esm-named.mjs'), { name: 'test registration options name' }) + await app.ready() + + const jsonToCompare = app.toJSON() + t.assert.strictEqual(jsonToCompare.label, 'root') + t.assert.strictEqual(jsonToCompare.nodes[0].label, 'test registration options name') +}) + +test('thenable plugins get a name from the resolved function name if theres no name in the options and no metadata', async (t) => { + t.plan(2) + const app = boot() + + app.use(import('./fixtures/esm-named.mjs')) + await app.ready() + + const jsonToCompare = app.toJSON() + t.assert.strictEqual(jsonToCompare.label, 'root') + t.assert.strictEqual(jsonToCompare.nodes[0].label, 'esmNamedPlugin') +}) diff --git a/test/plugin-timeout.test.js b/test/plugin-timeout.test.js index ebd59a46..ce2af3a4 100644 --- a/test/plugin-timeout.test.js +++ b/test/plugin-timeout.test.js @@ -74,6 +74,21 @@ test('timeout without calling next - use code as name', (t, done) => { }) }) +test('timeout without calling next - use the name of the resolved thenable', (t, done) => { + t.plan(3) + const app = boot({}, { + timeout: 10 // 10 ms + }) + app.use(import('./fixtures/esm-no-next.mjs')) + + app.ready((err) => { + t.assert.ok(err) + t.assert.strictEqual(err.message, message('esmPluginNoNext')) + t.assert.strictEqual(err.code, 'AVV_ERR_PLUGIN_EXEC_TIMEOUT') + done() + }) +}) + test('does not keep going', (t, done) => { t.plan(2) const app = boot({}, {