Skip to content

Commit fc6741e

Browse files
committed
only retry on network level errors, not http errors
1 parent 72b5c73 commit fc6741e

File tree

1 file changed

+50
-4
lines changed

1 file changed

+50
-4
lines changed

packages/cli/src/terminal.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,37 @@ function getStdoutSize() {
99
}
1010
}
1111

12+
function isRetryableError(err: unknown): boolean {
13+
// Retry on SDK TimeoutError
14+
if (err instanceof (e2b as any).TimeoutError) return true
15+
16+
// Some environments throw AbortError for aborted/timeout fetches
17+
if (err && typeof err === 'object' && (err as any).name === 'AbortError') return true
18+
19+
// Network/system-level transient errors commonly exposed via code property
20+
const code = (err as any)?.code ?? (err as any)?.cause?.code
21+
const retryableCodes = new Set([
22+
'ECONNRESET',
23+
'ECONNREFUSED',
24+
'ECONNABORTED',
25+
'EPIPE',
26+
'ETIMEDOUT',
27+
'ENOTFOUND',
28+
'EAI_AGAIN',
29+
'EHOSTUNREACH',
30+
'EADDRINUSE',
31+
])
32+
if (typeof code === 'string' && retryableCodes.has(code)) return true
33+
34+
// Undici/Fetch may surface as TypeError: fetch failed with nested cause
35+
if ((err as any) instanceof TypeError) {
36+
const msg = String((err as any).message || '').toLowerCase()
37+
if (msg.includes('fetch failed') || msg.includes('network error')) return true
38+
}
39+
40+
return false
41+
}
42+
1243
export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) {
1344
// Clear local terminal emulator before starting terminal
1445
// process.stdout.write('\x1b[2J\x1b[0f')
@@ -26,11 +57,25 @@ export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) {
2657

2758
const inputQueue = new BatchedQueue<Buffer>(async (batch) => {
2859
const combined = Buffer.concat(batch)
29-
await sandbox.pty.sendInput(terminalSession.pid, combined)
60+
61+
const maxRetries = 3
62+
let retry = 0
63+
do {
64+
try {
65+
await sandbox.pty.sendInput(terminalSession.pid, combined)
66+
break
67+
} catch (err) {
68+
if (!isRetryableError(err)) {
69+
// Do not retry on errors that come with valid HTTP/gRPC responses
70+
throw err
71+
}
72+
retry++
73+
}
74+
} while (retry < maxRetries)
3075
}, FLUSH_INPUT_INTERVAL_MS)
3176

3277
const resizeListener = process.stdout.on('resize', () =>
33-
sandbox.pty.resize(terminalSession.pid, getStdoutSize())
78+
sandbox.pty.resize(terminalSession.pid, getStdoutSize()),
3479
)
3580
const stdinListener = process.stdin.on('data', (data) => {
3681
inputQueue.push(data)
@@ -69,8 +114,9 @@ class BatchedQueue<T> {
69114

70115
constructor(
71116
private flushHandler: (batch: T[]) => Promise<void>,
72-
private flushIntervalMs: number
73-
) {}
117+
private flushIntervalMs: number,
118+
) {
119+
}
74120

75121
push(item: T) {
76122
this.queue.push(item)

0 commit comments

Comments
 (0)