Skip to content

Commit 4b76ba1

Browse files
authored
fix(sandbox): name a failed Function's error from its headline, not an inspected property (#8921)
* fix(sandbox): name a failed Function's error from its headline, not an inspected property A failed sandboxed Function named its error from the last trimmed `identifier: value` line of stderr. Node prints an error's own properties indented beneath it, so the error took the name of a property instead: a fetch that lost its socket was reported as `bytesRead: 0`, an ENOENT as its `path`, a custom error as one of its fields. Match only top-level error headlines: unindented, a space after the colon (so source-location headers like `node:internal/x:12` don't match), plus Node's `Name [CODE]: message` and bracketed `[Name: message] {` forms. Python tracebacks parse as before. * fix(sandbox): only read a bracketed headline as an error when it names an error type Node brackets other thrown values the same way it brackets a stackless error (`[Object: null prototype] {`, `[Function: handler]`), so the bracketed form now needs an Error/Exception name. * fix(sandbox): read a plain stackless Error and inline properties after a bracketed headline * fix(sandbox): unwrap a stackless headline whose message spans lines
1 parent ebc2214 commit 4b76ba1

2 files changed

Lines changed: 212 additions & 4 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { processCodeFailure } from '@/lib/execution/remote-sandbox/code-failure'
3+
4+
/** Node 22's crash report when `fetch` loses its socket before any response bytes. */
5+
const FETCH_SOCKET_CLOSED = `node:internal/deps/undici/undici:13510
6+
Error.captureStackTrace(err);
7+
^
8+
9+
TypeError: fetch failed
10+
at node:internal/deps/undici/undici:13510:13
11+
at async file:///home/user/.sim-function-abc.mjs:1:1 {
12+
[cause]: SocketError: other side closed
13+
at Socket.<anonymous> (node:internal/deps/undici/undici:6294:28)
14+
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
15+
code: 'UND_ERR_SOCKET',
16+
socket: {
17+
remotePort: 443,
18+
timeout: undefined,
19+
bytesWritten: 171,
20+
bytesRead: 0
21+
}
22+
}
23+
}
24+
25+
Node.js v22.19.0
26+
`
27+
28+
/** An error carrying its own properties, which Node prints indented after the stack. */
29+
const CUSTOM_ERROR_WITH_PROPERTIES = `file:///home/user/.sim-function-abc.mjs:1
30+
throw new QuotaExceeded('over quota')
31+
^
32+
33+
QuotaExceeded: over quota
34+
at file:///home/user/.sim-function-abc.mjs:1:109 {
35+
limit: 5
36+
}
37+
38+
Node.js v22.19.0
39+
`
40+
41+
const NODE_CODED_ERROR = `node:internal/modules/package_json_reader:266
42+
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
43+
^
44+
45+
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'left-pad' imported from /home/user/.sim-function-abc.mjs
46+
at packageResolve (node:internal/modules/esm/resolve:767:81) {
47+
code: 'ERR_MODULE_NOT_FOUND'
48+
}
49+
50+
Node.js v22.19.0
51+
`
52+
53+
/** An error printed without a stack (here a rejected `Promise.any`), bracketed by Node. */
54+
const STACKLESS_ERROR = `node:internal/modules/run_main:123
55+
triggerUncaughtException(
56+
^
57+
58+
[AggregateError: All promises were rejected] {
59+
[errors]: [
60+
Error: a
61+
at file:///home/user/.sim-function-abc.mjs:1:35,
62+
Error: b
63+
at file:///home/user/.sim-function-abc.mjs:1:67
64+
]
65+
}
66+
67+
Node.js v22.19.0
68+
`
69+
70+
/** A thrown non-error leaves only Node's source-location header, which is not an error name. */
71+
const THROWN_STRING = `
72+
node:internal/modules/run_main:123
73+
triggerUncaughtException(
74+
^
75+
just a string
76+
(Use \`node --trace-uncaught ...\` to show where the exception was thrown)
77+
78+
Node.js v22.19.0
79+
`
80+
81+
/** A thrown non-error object Node labels in brackets, like an error printed without a stack. */
82+
const THROWN_NULL_PROTOTYPE_OBJECT = `
83+
node:internal/modules/run_main:123
84+
triggerUncaughtException(
85+
^
86+
[Object: null prototype] {
87+
alpha: 'x',
88+
gamma: 3
89+
}
90+
91+
Node.js v22.19.0
92+
`
93+
94+
const THROWN_FUNCTION = `
95+
node:internal/modules/run_main:123
96+
triggerUncaughtException(
97+
^
98+
[Function: myHandler]
99+
100+
Node.js v22.19.0
101+
`
102+
103+
const STACKLESS_RANGE_ERROR = `file:///home/user/.sim-function-abc.mjs:1
104+
throw e
105+
^
106+
107+
[RangeError: limit hit]
108+
109+
Node.js v22.19.0
110+
`
111+
112+
const STACKLESS_PLAIN_ERROR = `file:///home/user/.sim-function-abc.mjs:1
113+
throw e
114+
^
115+
116+
[Error: plain failure]
117+
118+
Node.js v22.19.0
119+
`
120+
121+
const STACKLESS_ERROR_WITH_INLINE_PROPERTIES = `file:///home/user/.sim-function-abc.mjs:1
122+
throw e
123+
^
124+
125+
[TypeError: bad thing] { code: 'E1' }
126+
127+
Node.js v22.19.0
128+
`
129+
130+
const STACKLESS_MULTILINE_ERROR = `file:///home/user/.sim-function-abc.mjs:1
131+
throw e
132+
^
133+
134+
[TypeError: first line
135+
second line] {
136+
code: 'E2'
137+
}
138+
139+
Node.js v22.19.0
140+
`
141+
142+
const PYTHON_TRACEBACK = `Traceback (most recent call last):
143+
File "/home/user/.sim-function-abc.py", line 2, in <module>
144+
raise QuotaExceeded('over quota')
145+
__main__.QuotaExceeded: over quota
146+
`
147+
148+
describe('processCodeFailure', () => {
149+
it.each([
150+
['a fetch that lost its socket', FETCH_SOCKET_CLOSED, 'TypeError', 'fetch failed'],
151+
[
152+
'an error with its own properties',
153+
CUSTOM_ERROR_WITH_PROPERTIES,
154+
'QuotaExceeded',
155+
'over quota',
156+
],
157+
[
158+
"Node's coded error",
159+
NODE_CODED_ERROR,
160+
'Error [ERR_MODULE_NOT_FOUND]',
161+
"Cannot find package 'left-pad' imported from /home/user/.sim-function-abc.mjs",
162+
],
163+
[
164+
'an error printed without a stack',
165+
STACKLESS_ERROR,
166+
'AggregateError',
167+
'All promises were rejected',
168+
],
169+
['a stackless error with no properties', STACKLESS_RANGE_ERROR, 'RangeError', 'limit hit'],
170+
['a stackless plain Error', STACKLESS_PLAIN_ERROR, 'Error', 'plain failure'],
171+
[
172+
'a stackless error with inline properties',
173+
STACKLESS_ERROR_WITH_INLINE_PROPERTIES,
174+
'TypeError',
175+
'bad thing',
176+
],
177+
['a stackless multiline error', STACKLESS_MULTILINE_ERROR, 'TypeError', 'first line'],
178+
['a thrown string', THROWN_STRING, 'Error', 'Execution failed'],
179+
['a thrown null-prototype object', THROWN_NULL_PROTOTYPE_OBJECT, 'Error', 'Execution failed'],
180+
['a thrown function', THROWN_FUNCTION, 'Error', 'Execution failed'],
181+
['a Python traceback', PYTHON_TRACEBACK, '__main__.QuotaExceeded', 'over quota'],
182+
])('names the error of %s from its headline', (_case, stderr, name, value) => {
183+
const { error } = processCodeFailure({ stdout: '', stderr, exitCode: 1 })
184+
185+
expect(error).toEqual({ name, value, traceback: stderr })
186+
})
187+
188+
it('reads the traceback from stdout when stderr is empty', () => {
189+
const { error } = processCodeFailure({ stdout: PYTHON_TRACEBACK, stderr: '', exitCode: 1 })
190+
191+
expect(error?.name).toBe('__main__.QuotaExceeded')
192+
})
193+
})

‎apps/sim/lib/execution/remote-sandbox/code-failure.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
import type { SandboxCodeResult, SandboxCommandResult } from '@/lib/execution/remote-sandbox/types'
22

3+
/**
4+
* A top-level error headline in a Python traceback or Node crash report: `Name: message`,
5+
* Node's `Name [CODE]: message`, or `[NameError: message]` (any inline properties after it)
6+
* for an error printed without a stack. It starts the line, so the properties Node prints indented beneath an error
7+
* (`code: 'X'`, `bytesRead: 0`) never match, and a space follows the colon, so
8+
* source-location headers (`node:internal/x:12`, `file:///x.mjs:3`) don't either. The
9+
* bracketed form needs an error-type name, since Node brackets other thrown values the
10+
* same way (`[Object: null prototype] {`, `[Function: handler]`).
11+
*/
12+
const ERROR_HEADLINE =
13+
/^(?:\[(?:[A-Za-z_$][\w.$]*)?(?:Error|Exception)|[A-Za-z_$][\w.$]*(?: \[[\w.$-]+\])?):(?:\s|$)/
14+
315
/**
416
* Converts a failed code command into a code result, naming the error from the
5-
* last `Name: message` line of the traceback (stderr, else stdout).
17+
* last error headline of the traceback (stderr, else stdout).
618
*/
719
export function processCodeFailure(result: SandboxCommandResult): SandboxCodeResult {
820
const traceback = result.stderr || result.stdout
9-
const errorLine = traceback
21+
const headline = traceback
1022
.split('\n')
1123
.reverse()
12-
.find((line) => /^[A-Za-z_$][\w.$]*(?:Error|Exception|Interrupt|Exit)?:\s*/.test(line.trim()))
13-
?.trim()
24+
.find((line) => ERROR_HEADLINE.test(line))
25+
// A bracketed headline closes on this line, or on a later one for a multiline message.
26+
const errorLine = (
27+
headline?.startsWith('[') ? headline.slice(1).replace(/\](?: \{.*)?$/, '') : headline
28+
)?.trim()
1429
const separator = errorLine?.indexOf(':') ?? -1
1530
const parsedErrorLine = errorLine ?? ''
1631
const name = separator > 0 ? parsedErrorLine.slice(0, separator) : 'Error'

0 commit comments

Comments
 (0)