From b004ef5448376c3d5d7868b38ee27b3c2294d13d Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 09:42:08 +0100 Subject: [PATCH 01/10] Pull the error object out of isolated-vm when a user script throws an error. --- .../src/jsRunner/tests/jsRunner.spec.ts | 2 +- .../server/src/jsRunner/vm/isolated-vm.ts | 22 ++++++++++++++++++- .../src/helpers/javascript.ts | 3 +++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/server/src/jsRunner/tests/jsRunner.spec.ts b/packages/server/src/jsRunner/tests/jsRunner.spec.ts index 525f6d865e..2b9fc08041 100644 --- a/packages/server/src/jsRunner/tests/jsRunner.spec.ts +++ b/packages/server/src/jsRunner/tests/jsRunner.spec.ts @@ -44,7 +44,7 @@ describe("jsRunner (using isolated-vm)", () => { const output = await processJS( `return this.constructor.constructor("return process.env")()` ) - expect(output).toBe("Error while executing JS") + expect(output).toBe("ReferenceError: process is not defined") }) describe("helpers", () => { diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 87d5b966c2..e502b799bd 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -17,6 +17,15 @@ class ExecutionTimeoutError extends Error { } } +class UserScriptError extends Error { + constructor(readonly userScriptError: Error) { + super( + `error while running user-supplied JavaScript: ${userScriptError.message}`, + { cause: userScriptError } + ) + } +} + export class IsolatedVM implements VM { private isolate: ivm.Isolate private vm: ivm.Context @@ -29,6 +38,7 @@ export class IsolatedVM implements VM { private readonly resultKey = "results" private runResultKey: string + private runErrorKey: string constructor({ memoryLimit, @@ -47,6 +57,7 @@ export class IsolatedVM implements VM { this.jail.setSync("global", this.jail.derefInto()) this.runResultKey = crypto.randomUUID() + this.runErrorKey = crypto.randomUUID() this.addToContext({ [this.resultKey]: { [this.runResultKey]: "" }, }) @@ -216,7 +227,13 @@ export class IsolatedVM implements VM { } } - code = `results['${this.runResultKey}']=${this.codeWrapper(code)}` + code = ` + try { + results['${this.runResultKey}']=${this.codeWrapper(code)} + } catch (e) { + results['${this.runErrorKey}']=e + } + ` const script = this.isolate.compileScriptSync(code) @@ -227,6 +244,9 @@ export class IsolatedVM implements VM { // We can't rely on the script run result as it will not work for non-transferable values const result = this.getFromContext(this.resultKey) + if (result[this.runErrorKey]) { + throw new UserScriptError(result[this.runErrorKey]) + } return result[this.runResultKey] } diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 3e16d8a07b..27a71da834 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -100,6 +100,9 @@ export function processJS(handlebars: string, context: any) { if (error.name === "ExecutionTimeoutError") { return "Request JS execution limit hit" } + if ("userScriptError" in error) { + return error.userScriptError.toString() + } return "Error while executing JS" } } From fdbe633b02be6adfe6956a3c08347dcf193d06cb Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 12:43:57 +0100 Subject: [PATCH 02/10] Get errors working on the client side as well. --- .../bindings/EvaluationSidePanel.svelte | 27 ++++++++++++------- packages/string-templates/src/index.ts | 16 +++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index 2c4e6a0991..3b71fad715 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -8,22 +8,29 @@ export let evaluating = false export let expression = null - $: error = expressionResult === "Error while executing JS" + $: error = expressionResult && expressionResult.error != null $: empty = expression == null || expression?.trim() === "" $: success = !error && !empty $: highlightedResult = highlight(expressionResult) - const highlight = json => { - if (json == null) { + const highlight = result => { + if (result == null) { return "" } - // Attempt to parse and then stringify, in case this is valid JSON - try { - json = JSON.stringify(JSON.parse(json), null, 2) - } catch (err) { - // Ignore + + let str + if (result.error) { + str = result.error.toString() + } else { + // Attempt to parse and then stringify, in case this is valid result + try { + str = JSON.stringify(JSON.parse(result.result), null, 2) + } catch (err) { + // Ignore + } } - return formatHighlight(json, { + + return formatHighlight(str, { keyColor: "#e06c75", numberColor: "#e5c07b", stringColor: "#98c379", @@ -34,7 +41,7 @@ } const copy = () => { - let clipboardVal = expressionResult + let clipboardVal = expressionResult.result if (typeof clipboardVal === "object") { clipboardVal = JSON.stringify(clipboardVal, null, 2) } diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 8d5fe4c16d..69ca05ef76 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -463,6 +463,22 @@ export function defaultJSSetup() { setTimeout: undefined, } createContext(context) + + js = ` + result = { + result: null, + error: null, + }; + + try { + result.result = ${js}; + } catch (e) { + result.error = e.toString(); + } + + result; + ` + return runInNewContext(js, context, { timeout: 1000 }) }) } else { From df242cc2ad25978786b5da80045579fde989241c Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 13:07:22 +0100 Subject: [PATCH 03/10] Don't break the fact that processStringSync returns a string. --- .../common/bindings/BindingPanel.svelte | 16 ++++++--- .../bindings/EvaluationSidePanel.svelte | 33 +++++++++++-------- .../server/src/jsRunner/vm/isolated-vm.ts | 2 ++ .../src/utilities/rowProcessor/utils.ts | 9 ++++- packages/string-templates/src/errors.ts | 10 ++++++ .../src/helpers/javascript.ts | 4 +-- packages/string-templates/src/index.ts | 14 ++++++-- 7 files changed, 64 insertions(+), 24 deletions(-) diff --git a/packages/builder/src/components/common/bindings/BindingPanel.svelte b/packages/builder/src/components/common/bindings/BindingPanel.svelte index d8edf0cbb1..2f39dc6f53 100644 --- a/packages/builder/src/components/common/bindings/BindingPanel.svelte +++ b/packages/builder/src/components/common/bindings/BindingPanel.svelte @@ -66,6 +66,7 @@ let insertAtPos let targetMode = null let expressionResult + let expressionError let evaluating = false $: useSnippets = allowSnippets && !$licensing.isFreePlan @@ -142,10 +143,16 @@ } const debouncedEval = Utils.debounce((expression, context, snippets) => { - expressionResult = processStringSync(expression || "", { - ...context, - snippets, - }) + try { + expressionError = null + expressionResult = processStringSync(expression || "", { + ...context, + snippets, + }) + } catch (err) { + expressionResult = null + expressionError = err + } evaluating = false }, 260) @@ -370,6 +377,7 @@ {:else if sidePanel === SidePanels.Evaluation} diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index 3b71fad715..2c5c1d4587 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -5,32 +5,35 @@ import { fade } from "svelte/transition" export let expressionResult + export let expressionError export let evaluating = false export let expression = null - $: error = expressionResult && expressionResult.error != null + $: error = expressionError != null $: empty = expression == null || expression?.trim() === "" $: success = !error && !empty $: highlightedResult = highlight(expressionResult) - const highlight = result => { - if (result == null) { + const formatError = err => { + if (err.code === "USER_SCRIPT_ERROR") { + return err.userScriptError.toString() + } + return err.toString() + } + + const highlight = json => { + if (json == null) { return "" } - let str - if (result.error) { - str = result.error.toString() - } else { - // Attempt to parse and then stringify, in case this is valid result - try { - str = JSON.stringify(JSON.parse(result.result), null, 2) - } catch (err) { - // Ignore - } + // Attempt to parse and then stringify, in case this is valid result + try { + json = JSON.stringify(JSON.parse(json), null, 2) + } catch (err) { + // Ignore } - return formatHighlight(str, { + return formatHighlight(json, { keyColor: "#e06c75", numberColor: "#e5c07b", stringColor: "#98c379", @@ -80,6 +83,8 @@
{#if empty} Your expression will be evaluated here + {:else if error} + {formatError(expressionError)} {:else} {@html highlightedResult} diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index e502b799bd..9262687925 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -11,6 +11,7 @@ import { iifeWrapper } from "@budibase/string-templates" import environment from "../../environment" class ExecutionTimeoutError extends Error { + code = "ERR_SCRIPT_EXECUTION_TIMEOUT" constructor(message: string) { super(message) this.name = "ExecutionTimeoutError" @@ -18,6 +19,7 @@ class ExecutionTimeoutError extends Error { } class UserScriptError extends Error { + code = "USER_SCRIPT_ERROR" constructor(readonly userScriptError: Error) { super( `error while running user-supplied JavaScript: ${userScriptError.message}`, diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts index 0fa6f62807..3a1ce08ece 100644 --- a/packages/server/src/utilities/rowProcessor/utils.ts +++ b/packages/server/src/utilities/rowProcessor/utils.ts @@ -81,7 +81,14 @@ export async function processFormulas( ...row, [column]: tracer.trace("processStringSync", {}, span => { span?.addTags({ table_id: table._id, column, static: isStatic }) - return processStringSync(formula, context) + try { + return processStringSync(formula, context) + } catch (err: any) { + if (err.code === "USER_SCRIPT_ERROR") { + return err.userScriptError.toString() + } + throw err + } }), } } diff --git a/packages/string-templates/src/errors.ts b/packages/string-templates/src/errors.ts index 79a8a525ef..4c7cbdb360 100644 --- a/packages/string-templates/src/errors.ts +++ b/packages/string-templates/src/errors.ts @@ -1,3 +1,13 @@ export class JsErrorTimeout extends Error { code = "ERR_SCRIPT_EXECUTION_TIMEOUT" } + +export class UserScriptError extends Error { + code = "USER_SCRIPT_ERROR" + + constructor(readonly userScriptError: Error) { + super( + `error while running user-supplied JavaScript: ${userScriptError.message}` + ) + } +} diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 27a71da834..e1d7f3e8a3 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -100,8 +100,8 @@ export function processJS(handlebars: string, context: any) { if (error.name === "ExecutionTimeoutError") { return "Request JS execution limit hit" } - if ("userScriptError" in error) { - return error.userScriptError.toString() + if (error.code === "USER_SCRIPT_ERROR") { + throw error } return "Error while executing JS" } diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 69ca05ef76..aceea1da7e 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -16,6 +16,7 @@ import { removeJSRunner, setJSRunner } from "./helpers/javascript" import manifest from "./manifest.json" import { ProcessOptions } from "./types" +import { UserScriptError } from "./errors" export { helpersToRemoveForJs, getJsHelperList } from "./helpers/list" export { FIND_ANY_HBS_REGEX } from "./utilities" @@ -230,6 +231,9 @@ export function processStringSync( return process(string) } } catch (err) { + if (err.code === "USER_SCRIPT_ERROR") { + throw err + } return input } } @@ -448,7 +452,7 @@ export function convertToJS(hbs: string) { return `${varBlock}${js}` } -export { JsErrorTimeout } from "./errors" +export { JsErrorTimeout, UserScriptError } from "./errors" export function defaultJSSetup() { if (!isBackendService()) { @@ -473,13 +477,17 @@ export function defaultJSSetup() { try { result.result = ${js}; } catch (e) { - result.error = e.toString(); + result.error = e; } result; ` - return runInNewContext(js, context, { timeout: 1000 }) + const result = runInNewContext(js, context, { timeout: 1000 }) + if (result.error) { + throw new UserScriptError(result.error) + } + return result.result }) } else { removeJSRunner() From f9ccbbe081b447c36ae8b472dc64124833fb8c79 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 13:11:01 +0100 Subject: [PATCH 04/10] Fix jsRunner.spec.ts. --- packages/server/src/jsRunner/tests/jsRunner.spec.ts | 7 ++++--- packages/server/src/jsRunner/vm/isolated-vm.ts | 2 +- packages/string-templates/src/errors.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/server/src/jsRunner/tests/jsRunner.spec.ts b/packages/server/src/jsRunner/tests/jsRunner.spec.ts index 2b9fc08041..7d2df17c1f 100644 --- a/packages/server/src/jsRunner/tests/jsRunner.spec.ts +++ b/packages/server/src/jsRunner/tests/jsRunner.spec.ts @@ -41,10 +41,11 @@ describe("jsRunner (using isolated-vm)", () => { }) it("should prevent sandbox escape", async () => { - const output = await processJS( - `return this.constructor.constructor("return process.env")()` + await expect( + processJS(`return this.constructor.constructor("return process.env")()`) + ).rejects.toThrow( + "error while running user-supplied JavaScript: ReferenceError: process is not defined" ) - expect(output).toBe("ReferenceError: process is not defined") }) describe("helpers", () => { diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 9262687925..0a9e93f475 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -22,7 +22,7 @@ class UserScriptError extends Error { code = "USER_SCRIPT_ERROR" constructor(readonly userScriptError: Error) { super( - `error while running user-supplied JavaScript: ${userScriptError.message}`, + `error while running user-supplied JavaScript: ${userScriptError.toString()}`, { cause: userScriptError } ) } diff --git a/packages/string-templates/src/errors.ts b/packages/string-templates/src/errors.ts index 4c7cbdb360..d68461561e 100644 --- a/packages/string-templates/src/errors.ts +++ b/packages/string-templates/src/errors.ts @@ -7,7 +7,7 @@ export class UserScriptError extends Error { constructor(readonly userScriptError: Error) { super( - `error while running user-supplied JavaScript: ${userScriptError.message}` + `error while running user-supplied JavaScript: ${userScriptError.toString()}` ) } } From 795b10c4c9145d15c78ae708b51af44de179b924 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 13:11:35 +0100 Subject: [PATCH 05/10] Fix types in string-templates. --- packages/string-templates/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index aceea1da7e..8fd2e7bfc4 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -230,7 +230,7 @@ export function processStringSync( } else { return process(string) } - } catch (err) { + } catch (err: any) { if (err.code === "USER_SCRIPT_ERROR") { throw err } From 646fc5e6bdde971b5c08b62bb0b529022740f9de Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 15:56:19 +0100 Subject: [PATCH 06/10] Respond to PR comment. --- .../bindings/EvaluationSidePanel.svelte | 3 ++- .../server/src/jsRunner/vm/isolated-vm.ts | 26 +++++-------------- .../src/utilities/rowProcessor/utils.ts | 4 +-- packages/string-templates/src/errors.ts | 4 +-- .../src/helpers/javascript.ts | 8 +++--- packages/string-templates/src/index.ts | 6 ++--- 6 files changed, 18 insertions(+), 33 deletions(-) diff --git a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte index 2c5c1d4587..ffb8f45297 100644 --- a/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte +++ b/packages/builder/src/components/common/bindings/EvaluationSidePanel.svelte @@ -3,6 +3,7 @@ import { Icon, ProgressCircle, notifications } from "@budibase/bbui" import { copyToClipboard } from "@budibase/bbui/helpers" import { fade } from "svelte/transition" + import { UserScriptError } from "@budibase/string-templates" export let expressionResult export let expressionError @@ -15,7 +16,7 @@ $: highlightedResult = highlight(expressionResult) const formatError = err => { - if (err.code === "USER_SCRIPT_ERROR") { + if (err.code === UserScriptError.code) { return err.userScriptError.toString() } return err.toString() diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 0a9e93f475..87478a2f8a 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -7,27 +7,13 @@ import querystring from "querystring" import { BundleType, loadBundle } from "../bundles" import { Snippet, VM } from "@budibase/types" -import { iifeWrapper } from "@budibase/string-templates" +import { + iifeWrapper, + JsErrorTimeout, + UserScriptError, +} from "@budibase/string-templates" import environment from "../../environment" -class ExecutionTimeoutError extends Error { - code = "ERR_SCRIPT_EXECUTION_TIMEOUT" - constructor(message: string) { - super(message) - this.name = "ExecutionTimeoutError" - } -} - -class UserScriptError extends Error { - code = "USER_SCRIPT_ERROR" - constructor(readonly userScriptError: Error) { - super( - `error while running user-supplied JavaScript: ${userScriptError.toString()}`, - { cause: userScriptError } - ) - } -} - export class IsolatedVM implements VM { private isolate: ivm.Isolate private vm: ivm.Context @@ -223,7 +209,7 @@ export class IsolatedVM implements VM { if (this.isolateAccumulatedTimeout) { const cpuMs = Number(this.isolate.cpuTime) / 1e6 if (cpuMs > this.isolateAccumulatedTimeout) { - throw new ExecutionTimeoutError( + throw new JsErrorTimeout( `CPU time limit exceeded (${cpuMs}ms > ${this.isolateAccumulatedTimeout}ms)` ) } diff --git a/packages/server/src/utilities/rowProcessor/utils.ts b/packages/server/src/utilities/rowProcessor/utils.ts index 3a1ce08ece..29a5de221f 100644 --- a/packages/server/src/utilities/rowProcessor/utils.ts +++ b/packages/server/src/utilities/rowProcessor/utils.ts @@ -1,5 +1,5 @@ import { AutoFieldDefaultNames } from "../../constants" -import { processStringSync } from "@budibase/string-templates" +import { processStringSync, UserScriptError } from "@budibase/string-templates" import { AutoColumnFieldMetadata, FieldSchema, @@ -84,7 +84,7 @@ export async function processFormulas( try { return processStringSync(formula, context) } catch (err: any) { - if (err.code === "USER_SCRIPT_ERROR") { + if (err.code === UserScriptError.code) { return err.userScriptError.toString() } throw err diff --git a/packages/string-templates/src/errors.ts b/packages/string-templates/src/errors.ts index d68461561e..7a300ac863 100644 --- a/packages/string-templates/src/errors.ts +++ b/packages/string-templates/src/errors.ts @@ -1,9 +1,9 @@ export class JsErrorTimeout extends Error { - code = "ERR_SCRIPT_EXECUTION_TIMEOUT" + static code = "ERR_SCRIPT_EXECUTION_TIMEOUT" } export class UserScriptError extends Error { - code = "USER_SCRIPT_ERROR" + static code = "USER_SCRIPT_ERROR" constructor(readonly userScriptError: Error) { super( diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index e1d7f3e8a3..059c8a95f5 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -3,6 +3,7 @@ import cloneDeep from "lodash/fp/cloneDeep" import { LITERAL_MARKER } from "../helpers/constants" import { getJsHelperList } from "./list" import { iifeWrapper } from "../iife" +import { JsErrorTimeout, UserScriptError } from "../errors" // The method of executing JS scripts depends on the bundle being built. // This setter is used in the entrypoint (either index.js or index.mjs). @@ -94,13 +95,10 @@ export function processJS(handlebars: string, context: any) { } catch (error: any) { onErrorLog && onErrorLog(error) - if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") { + if (error.code === JsErrorTimeout.code) { return "Timed out while executing JS" } - if (error.name === "ExecutionTimeoutError") { - return "Request JS execution limit hit" - } - if (error.code === "USER_SCRIPT_ERROR") { + if (error.code === UserScriptError.code) { throw error } return "Error while executing JS" diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index 8fd2e7bfc4..fe2b4c261c 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -231,7 +231,7 @@ export function processStringSync( return process(string) } } catch (err: any) { - if (err.code === "USER_SCRIPT_ERROR") { + if (err.code === UserScriptError.code) { throw err } return input @@ -468,7 +468,7 @@ export function defaultJSSetup() { } createContext(context) - js = ` + const wrappedJs = ` result = { result: null, error: null, @@ -483,7 +483,7 @@ export function defaultJSSetup() { result; ` - const result = runInNewContext(js, context, { timeout: 1000 }) + const result = runInNewContext(wrappedJs, context, { timeout: 1000 }) if (result.error) { throw new UserScriptError(result.error) } From 03c514be4c6e8effb1ec657282d6c5415eaf5e78 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 16:16:42 +0100 Subject: [PATCH 07/10] Fix tests. --- .../server/src/api/routes/tests/row.spec.ts | 17 +++++++++-------- packages/server/src/jsRunner/index.ts | 4 ++-- packages/server/src/jsRunner/vm/isolated-vm.ts | 13 +++++++------ packages/string-templates/src/errors.ts | 11 +++++++++-- .../string-templates/src/helpers/javascript.ts | 9 ++++++--- packages/string-templates/src/index.ts | 2 +- 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index f751942df9..7b590befc0 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -49,6 +49,7 @@ import * as uuid from "uuid" import { Knex } from "knex" import { InternalTables } from "../../../db/utils" import { withEnv } from "../../../environment" +import { JsTimeoutError } from "@budibase/string-templates/src/errors" const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() tk.freeze(timestamp) @@ -76,13 +77,13 @@ async function waitForEvent( } describe.each([ - ["lucene", undefined], + // ["lucene", undefined], ["sqs", undefined], - [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], + // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], + // [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], ])("/rows (%s)", (providerType, dsProvider) => { const isInternal = dsProvider === undefined const isLucene = providerType === "lucene" @@ -3013,7 +3014,7 @@ describe.each([ let i = 0 for (; i < 10; i++) { const row = rows[i] - if (row.formula !== "Timed out while executing JS") { + if (row.formula !== JsTimeoutError.message) { break } } @@ -3027,7 +3028,7 @@ describe.each([ for (; i < 10; i++) { const row = rows[i] expect(row.text).toBe("foo") - expect(row.formula).toBe("Request JS execution limit hit") + expect(row.formula).toStartWith("CPU time limit exceeded ") } } } diff --git a/packages/server/src/jsRunner/index.ts b/packages/server/src/jsRunner/index.ts index ccfca7cb4c..e17529a687 100644 --- a/packages/server/src/jsRunner/index.ts +++ b/packages/server/src/jsRunner/index.ts @@ -1,7 +1,7 @@ import { serializeError } from "serialize-error" import env from "../environment" import { - JsErrorTimeout, + JsTimeoutError, setJSRunner, setOnErrorLog, } from "@budibase/string-templates" @@ -40,7 +40,7 @@ export function init() { return vm.withContext(rest, () => vm.execute(js)) } catch (error: any) { if (error.message === "Script execution timed out.") { - throw new JsErrorTimeout() + throw new JsTimeoutError() } throw error } diff --git a/packages/server/src/jsRunner/vm/isolated-vm.ts b/packages/server/src/jsRunner/vm/isolated-vm.ts index 87478a2f8a..b8ed90aa23 100644 --- a/packages/server/src/jsRunner/vm/isolated-vm.ts +++ b/packages/server/src/jsRunner/vm/isolated-vm.ts @@ -7,13 +7,14 @@ import querystring from "querystring" import { BundleType, loadBundle } from "../bundles" import { Snippet, VM } from "@budibase/types" -import { - iifeWrapper, - JsErrorTimeout, - UserScriptError, -} from "@budibase/string-templates" +import { iifeWrapper, UserScriptError } from "@budibase/string-templates" import environment from "../../environment" +export class JsRequestTimeoutError extends Error { + static code = "JS_REQUEST_TIMEOUT_ERROR" + code = JsRequestTimeoutError.code +} + export class IsolatedVM implements VM { private isolate: ivm.Isolate private vm: ivm.Context @@ -209,7 +210,7 @@ export class IsolatedVM implements VM { if (this.isolateAccumulatedTimeout) { const cpuMs = Number(this.isolate.cpuTime) / 1e6 if (cpuMs > this.isolateAccumulatedTimeout) { - throw new JsErrorTimeout( + throw new JsRequestTimeoutError( `CPU time limit exceeded (${cpuMs}ms > ${this.isolateAccumulatedTimeout}ms)` ) } diff --git a/packages/string-templates/src/errors.ts b/packages/string-templates/src/errors.ts index 7a300ac863..77526c1b96 100644 --- a/packages/string-templates/src/errors.ts +++ b/packages/string-templates/src/errors.ts @@ -1,9 +1,16 @@ -export class JsErrorTimeout extends Error { - static code = "ERR_SCRIPT_EXECUTION_TIMEOUT" +export class JsTimeoutError extends Error { + static message = "Timed out while executing JS" + static code = "JS_TIMEOUT_ERROR" + code: string = JsTimeoutError.code + + constructor() { + super(JsTimeoutError.message) + } } export class UserScriptError extends Error { static code = "USER_SCRIPT_ERROR" + code: string = UserScriptError.code constructor(readonly userScriptError: Error) { super( diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 059c8a95f5..879cd29fa6 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -3,7 +3,7 @@ import cloneDeep from "lodash/fp/cloneDeep" import { LITERAL_MARKER } from "../helpers/constants" import { getJsHelperList } from "./list" import { iifeWrapper } from "../iife" -import { JsErrorTimeout, UserScriptError } from "../errors" +import { JsTimeoutError, UserScriptError } from "../errors" // The method of executing JS scripts depends on the bundle being built. // This setter is used in the entrypoint (either index.js or index.mjs). @@ -95,8 +95,11 @@ export function processJS(handlebars: string, context: any) { } catch (error: any) { onErrorLog && onErrorLog(error) - if (error.code === JsErrorTimeout.code) { - return "Timed out while executing JS" + if (error.code === "JS_REQUEST_TIMEOUT_ERROR") { + return error.message + } + if (error.code === JsTimeoutError.code) { + return JsTimeoutError.message } if (error.code === UserScriptError.code) { throw error diff --git a/packages/string-templates/src/index.ts b/packages/string-templates/src/index.ts index fe2b4c261c..0f5326374f 100644 --- a/packages/string-templates/src/index.ts +++ b/packages/string-templates/src/index.ts @@ -452,7 +452,7 @@ export function convertToJS(hbs: string) { return `${varBlock}${js}` } -export { JsErrorTimeout, UserScriptError } from "./errors" +export { JsTimeoutError, UserScriptError } from "./errors" export function defaultJSSetup() { if (!isBackendService()) { From 15a30b1d9ef30b14770ef8ab2ade298bcdee98ab Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Thu, 3 Oct 2024 16:32:14 +0100 Subject: [PATCH 08/10] Fix yet more tests. --- .../server/src/api/routes/tests/row.spec.ts | 14 +++++------ .../src/helpers/javascript.ts | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 7b590befc0..5222069460 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -49,7 +49,7 @@ import * as uuid from "uuid" import { Knex } from "knex" import { InternalTables } from "../../../db/utils" import { withEnv } from "../../../environment" -import { JsTimeoutError } from "@budibase/string-templates/src/errors" +import { JsTimeoutError } from "@budibase/string-templates" const timestamp = new Date("2023-01-26T11:48:57.597Z").toISOString() tk.freeze(timestamp) @@ -77,13 +77,13 @@ async function waitForEvent( } describe.each([ - // ["lucene", undefined], + ["lucene", undefined], ["sqs", undefined], - // [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], - // [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], - // [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], - // [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], - // [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], + [DatabaseName.POSTGRES, getDatasource(DatabaseName.POSTGRES)], + [DatabaseName.MYSQL, getDatasource(DatabaseName.MYSQL)], + [DatabaseName.SQL_SERVER, getDatasource(DatabaseName.SQL_SERVER)], + [DatabaseName.MARIADB, getDatasource(DatabaseName.MARIADB)], + [DatabaseName.ORACLE, getDatasource(DatabaseName.ORACLE)], ])("/rows (%s)", (providerType, dsProvider) => { const isInternal = dsProvider === undefined const isLucene = providerType === "lucene" diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index 879cd29fa6..d5183ef3ed 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -95,15 +95,38 @@ export function processJS(handlebars: string, context: any) { } catch (error: any) { onErrorLog && onErrorLog(error) + // The error handling below is quite messy, because it has fallen to + // string-templates to handle a variety of types of error specific to usages + // above it in the stack. It would be nice some day to refactor this to + // allow each user of processStringSync to handle errors in the way they see + // fit. + + // This is to catch the error vm.runInNewContext() throws when the timeout + // is exceeded. + if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") { + return "Timed out while executing JS" + } + + // This is to catch the JsRequestTimeoutError we throw when we detect a + // timeout across an entire request in the backend. We use a magic string + // because we can't import from the backend into string-templates. if (error.code === "JS_REQUEST_TIMEOUT_ERROR") { return error.message } + + // This is to catch the JsTimeoutError we throw when we detect a timeout in + // a single JS execution. if (error.code === JsTimeoutError.code) { return JsTimeoutError.message } + + // This is to catch the error that happens if a user-supplied JS script + // throws for reasons introduced by the user. if (error.code === UserScriptError.code) { throw error } + + // If all else fails, generic error message. return "Error while executing JS" } } From 831c81a99c3aa701d817ffa878d67f3073560497 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 4 Oct 2024 09:31:42 +0100 Subject: [PATCH 09/10] Fix automation tests. --- packages/server/src/api/controllers/script.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server/src/api/controllers/script.ts b/packages/server/src/api/controllers/script.ts index 4317f5fcde..0565b30f74 100644 --- a/packages/server/src/api/controllers/script.ts +++ b/packages/server/src/api/controllers/script.ts @@ -1,11 +1,18 @@ import { Ctx } from "@budibase/types" import { IsolatedVM } from "../../jsRunner/vm" -import { iifeWrapper } from "@budibase/string-templates" +import { iifeWrapper, UserScriptError } from "@budibase/string-templates" export async function execute(ctx: Ctx) { const { script, context } = ctx.request.body const vm = new IsolatedVM() - ctx.body = vm.withContext(context, () => vm.execute(iifeWrapper(script))) + try { + ctx.body = vm.withContext(context, () => vm.execute(iifeWrapper(script))) + } catch (err: any) { + if (err.code === UserScriptError.code) { + throw err.userScriptError + } + throw err + } } export async function save(ctx: Ctx) { From 28e6a039292e65b1df9347671999aa858253747c Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 4 Oct 2024 10:32:33 +0100 Subject: [PATCH 10/10] Include syntax errors in processJS --- packages/string-templates/src/helpers/javascript.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/string-templates/src/helpers/javascript.ts b/packages/string-templates/src/helpers/javascript.ts index d5183ef3ed..ed8d4ad6d7 100644 --- a/packages/string-templates/src/helpers/javascript.ts +++ b/packages/string-templates/src/helpers/javascript.ts @@ -126,7 +126,10 @@ export function processJS(handlebars: string, context: any) { throw error } - // If all else fails, generic error message. + if (error.name === "SyntaxError") { + return error.toString() + } + return "Error while executing JS" } }