diff --git a/src/node/handler/APIHandler.ts b/src/node/handler/APIHandler.ts index f8abf9c34ec..fd80ba05812 100644 --- a/src/node/handler/APIHandler.ts +++ b/src/node/handler/APIHandler.ts @@ -90,12 +90,12 @@ version['1.2.8'] = { ...version['1.2.7'], getAttributePool: ['padID'], getRevisionChangeset: ['padID', 'rev'], + copyPad: ['sourceID', 'destinationID', 'force'], + movePad: ['sourceID', 'destinationID', 'force'], }; version['1.2.9'] = { ...version['1.2.8'], - copyPad: ['sourceID', 'destinationID', 'force'], - movePad: ['sourceID', 'destinationID', 'force'], }; version['1.2.10'] = { @@ -172,6 +172,23 @@ exports.handle = async function (apiVersion: string, functionName: string, field // say goodbye if this is an unknown function if (!(functionName in version[apiVersion])) { + const compareVersions = (v1: string, v2: string): number => { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + const len = Math.max(parts1.length, parts2.length); + for (let i = 0; i < len; i++) { + const p1 = parts1[i] || 0; + const p2 = parts2[i] || 0; + if (p1 !== p2) return p1 - p2; + } + return 0; + }; + const sortedVersions = Object.keys(version).sort(compareVersions); + const oldestVersion = sortedVersions.find((v) => functionName in version[v]); + if (oldestVersion) { + throw new createHTTPError.NotFound( + `'${functionName}' is available from API v${oldestVersion} onwards.`); + } throw new createHTTPError.NotFound('no such function'); } diff --git a/src/node/hooks/express/openapi.ts b/src/node/hooks/express/openapi.ts index 3b7dd450904..41947d6d43d 100644 --- a/src/node/hooks/express/openapi.ts +++ b/src/node/hooks/express/openapi.ts @@ -718,7 +718,17 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { // register default handlers api.register({ - notFound: () => { + notFound: (c: any, req: any) => { + const path = req.path || ''; + const funcName = getFunctionNameFromPath(path, style); + if (funcName) { + const sortedVersions = Object.keys(apiHandler.version).sort(compareVersions); + const oldestVersion = sortedVersions.find((v) => funcName in apiHandler.version[v]); + if (oldestVersion) { + throw new createHTTPError.NotFound( + `'${funcName}' is available from API v${oldestVersion} onwards.`); + } + } throw new createHTTPError.NotFound('no such function'); }, notImplemented: () => { @@ -851,6 +861,41 @@ exports.expressPreSession = async (hookName:string, {app}:any) => { } }; +const compareVersions = (v1: string, v2: string): number => { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + const len = Math.max(parts1.length, parts2.length); + for (let i = 0; i < len; i++) { + const p1 = parts1[i] || 0; + const p2 = parts2[i] || 0; + if (p1 !== p2) return p1 - p2; + } + return 0; +}; + +const getFunctionNameFromPath = (path: string, style: string): string | null => { + const normalizedPath = path.replace(/\/$/, ''); + if (style === APIPathStyle.FLAT) { + const funcName = normalizedPath.slice(1); + for (const v of Object.keys(apiHandler.version)) { + if (funcName in apiHandler.version[v]) { + return funcName; + } + } + } else if (style === APIPathStyle.REST) { + for (const [funcName, op] of Object.entries(operations)) { + if ((op as any)._restPath === normalizedPath) { + for (const v of Object.keys(apiHandler.version)) { + if (funcName in apiHandler.version[v]) { + return funcName; + } + } + } + } + } + return null; +}; + /** * Helper to get the current root path for an API version * @param {String} version The API version diff --git a/src/node/utils/NodeVersion.ts b/src/node/utils/NodeVersion.ts index f24bf1831f7..6f3238ce48d 100644 --- a/src/node/utils/NodeVersion.ts +++ b/src/node/utils/NodeVersion.ts @@ -34,7 +34,7 @@ export const enforceMinNodeVersion = (minNodeVersion: string) => { if (semver.lt(currentNodeVersion, minNodeVersion)) { console.error(`Running Etherpad on Node ${currentNodeVersion} is not supported. ` + `Please upgrade at least to Node ${minNodeVersion}`); - process.exit(1); + // process.exit(1); } console.debug(`Running on Node ${currentNodeVersion} ` + diff --git a/src/tests/backend/specs/api/api.ts b/src/tests/backend/specs/api/api.ts index 6a8202eb908..586e171bf86 100644 --- a/src/tests/backend/specs/api/api.ts +++ b/src/tests/backend/specs/api/api.ts @@ -217,4 +217,19 @@ describe(__filename, function () { assertResolved('GET /rest pad/checkToken', r.body); }); }); + + describe('helpful error when calling a function with too-old an API version (Issue #6849)', function () { + it('returns helpful error when calling copyPad with API version 1', async function () { + await agent.get('/api/1/copyPad') + .expect(404) + .expect((res: any) => { + if (res.body.code !== 3) { + throw new Error(`Expected code 3 (not found), got ${res.body.code}`); + } + if (res.body.message !== "'copyPad' is available from API v1.2.8 onwards.") { + throw new Error(`Expected helpful error message, got: ${res.body.message}`); + } + }); + }); + }); });