From 1450cbb14b45990c1bc66ca357d8b15fbff32c9f Mon Sep 17 00:00:00 2001 From: Maxwell Calkin Date: Sun, 8 Mar 2026 22:23:52 -0400 Subject: [PATCH 1/2] feat: add type property to TimeoutException/TimeoutError for programmatic timeout identification Fixes #463. Previously, users had to parse the error message string to determine what kind of timeout occurred (sandbox vs request vs execution). Now both the Python and JS SDKs expose a `type` property (TimeoutType enum) on timeout exceptions, enabling clean programmatic handling. Co-Authored-By: Claude Opus 4.6 --- packages/js-sdk/src/envd/rpc.ts | 9 +++-- packages/js-sdk/src/errors.ts | 48 ++++++++++++++++++++++----- packages/js-sdk/src/index.ts | 1 + packages/python-sdk/e2b/__init__.py | 2 ++ packages/python-sdk/e2b/envd/rpc.py | 7 ++-- packages/python-sdk/e2b/exceptions.py | 35 +++++++++++++++---- 6 files changed, 84 insertions(+), 18 deletions(-) diff --git a/packages/js-sdk/src/envd/rpc.ts b/packages/js-sdk/src/envd/rpc.ts index 9ad34ec6e8..e0e609b1d9 100644 --- a/packages/js-sdk/src/envd/rpc.ts +++ b/packages/js-sdk/src/envd/rpc.ts @@ -10,6 +10,7 @@ import { NotFoundError, SandboxError, TimeoutError, + TimeoutType, } from '../errors' import { ENVD_DEFAULT_USER } from './versions' @@ -26,11 +27,15 @@ export function handleRpcError(err: unknown): Error { return formatSandboxTimeoutError(err.message) case Code.Canceled: return new TimeoutError( - `${err.message}: This error is likely due to exceeding 'requestTimeoutMs'. You can pass the request timeout value as an option when making the request.` + `${err.message}: This error is likely due to exceeding 'requestTimeoutMs'. You can pass the request timeout value as an option when making the request.`, + undefined, + TimeoutType.REQUEST ) case Code.DeadlineExceeded: return new TimeoutError( - `${err.message}: This error is likely due to exceeding 'timeoutMs' — the total time a long running request (like command execution or directory watch) can be active. It can be modified by passing 'timeoutMs' when making the request. Use '0' to disable the timeout.` + `${err.message}: This error is likely due to exceeding 'timeoutMs' — the total time a long running request (like command execution or directory watch) can be active. It can be modified by passing 'timeoutMs' when making the request. Use '0' to disable the timeout.`, + undefined, + TimeoutType.EXECUTION ) default: return new SandboxError(`${err.code}: ${err.message}`) diff --git a/packages/js-sdk/src/errors.ts b/packages/js-sdk/src/errors.ts index ad51c1f2a4..35438e018a 100644 --- a/packages/js-sdk/src/errors.ts +++ b/packages/js-sdk/src/errors.ts @@ -1,7 +1,31 @@ +/** + * The type of timeout that occurred. + * + * - `sandbox` — the sandbox itself timed out (e.g., idle timeout expired). + * - `request` — the HTTP request timed out (exceeded `requestTimeoutMs`). + * - `execution` — a long-running operation timed out (exceeded `timeoutMs` for command execution, watch, etc.). + */ +export enum TimeoutType { + /** + * The sandbox itself timed out (e.g., idle timeout expired). + */ + SANDBOX = 'sandbox', + /** + * The HTTP request timed out (exceeded `requestTimeoutMs`). + */ + REQUEST = 'request', + /** + * A long-running operation timed out (exceeded `timeoutMs` for command execution, watch, etc.). + */ + EXECUTION = 'execution', +} + // This is the message for the sandbox timeout error when the response code is 502/Unavailable export function formatSandboxTimeoutError(message: string) { return new TimeoutError( - `${message}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeoutMs' when starting the sandbox or calling '.setTimeout' on the sandbox with the desired timeout.` + `${message}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeoutMs' when starting the sandbox or calling '.setTimeout' on the sandbox with the desired timeout.`, + undefined, + TimeoutType.SANDBOX ) } @@ -23,18 +47,26 @@ export class SandboxError extends Error { /** * Thrown when a timeout error occurs. * - * The [unavailable] error type is caused by sandbox timeout. + * The {@link type} property indicates the kind of timeout: * - * The [canceled] error type is caused by exceeding request timeout. - * - * The [deadline_exceeded] error type is caused by exceeding the timeout for command execution, watch, etc. - * - * The [unknown] error type is sometimes caused by the sandbox timeout when the request is not processed correctly. + * - {@link TimeoutType.SANDBOX} — the sandbox itself timed out (idle timeout, etc.). + * - {@link TimeoutType.REQUEST} — the HTTP request exceeded `requestTimeoutMs`. + * - {@link TimeoutType.EXECUTION} — a long-running operation exceeded its `timeoutMs`. */ export class TimeoutError extends SandboxError { - constructor(message: string, stackTrace?: string) { + /** + * The type of timeout that occurred. + */ + readonly type: TimeoutType + + constructor( + message: string, + stackTrace?: string, + type: TimeoutType = TimeoutType.SANDBOX + ) { super(message, stackTrace) this.name = 'TimeoutError' + this.type = type } } diff --git a/packages/js-sdk/src/index.ts b/packages/js-sdk/src/index.ts index 17d4a97527..16d1269949 100644 --- a/packages/js-sdk/src/index.ts +++ b/packages/js-sdk/src/index.ts @@ -13,6 +13,7 @@ export { SandboxError, TemplateError, TimeoutError, + TimeoutType, RateLimitError, BuildError, FileUploadError, diff --git a/packages/python-sdk/e2b/__init__.py b/packages/python-sdk/e2b/__init__.py index 264fd5deee..12099f5293 100644 --- a/packages/python-sdk/e2b/__init__.py +++ b/packages/python-sdk/e2b/__init__.py @@ -45,6 +45,7 @@ SandboxException, TemplateException, TimeoutException, + TimeoutType, ) from .sandbox.commands.command_handle import ( CommandExitException, @@ -121,6 +122,7 @@ # Exceptions "SandboxException", "TimeoutException", + "TimeoutType", "NotFoundException", "AuthenticationException", "GitAuthException", diff --git a/packages/python-sdk/e2b/envd/rpc.py b/packages/python-sdk/e2b/envd/rpc.py index f8b2f3b9f8..3eecb4e068 100644 --- a/packages/python-sdk/e2b/envd/rpc.py +++ b/packages/python-sdk/e2b/envd/rpc.py @@ -9,6 +9,7 @@ InvalidArgumentException, NotFoundException, TimeoutException, + TimeoutType, format_sandbox_timeout_exception, AuthenticationException, RateLimitException, @@ -33,11 +34,13 @@ def handle_rpc_exception(e: Exception): ) elif e.status == Code.canceled: return TimeoutException( - f"{e.message}: This error is likely due to exceeding 'request_timeout'. You can pass the request timeout value as an option when making the request." + f"{e.message}: This error is likely due to exceeding 'request_timeout'. You can pass the request timeout value as an option when making the request.", + type=TimeoutType.REQUEST, ) elif e.status == Code.deadline_exceeded: return TimeoutException( - f"{e.message}: This error is likely due to exceeding 'timeout' — the total time a long running request (like process or directory watch) can be active. It can be modified by passing 'timeout' when making the request. Use '0' to disable the timeout." + f"{e.message}: This error is likely due to exceeding 'timeout' — the total time a long running request (like process or directory watch) can be active. It can be modified by passing 'timeout' when making the request. Use '0' to disable the timeout.", + type=TimeoutType.EXECUTION, ) else: return SandboxException(f"{e.status}: {e.message}") diff --git a/packages/python-sdk/e2b/exceptions.py b/packages/python-sdk/e2b/exceptions.py index 75f5f6abf0..c0b1a99ce9 100644 --- a/packages/python-sdk/e2b/exceptions.py +++ b/packages/python-sdk/e2b/exceptions.py @@ -1,18 +1,38 @@ +from enum import Enum + + +class TimeoutType(str, Enum): + """ + The type of timeout that occurred. + + - ``SANDBOX``: The sandbox itself timed out (e.g., idle timeout expired). + - ``REQUEST``: The HTTP request timed out (exceeded ``request_timeout``). + - ``EXECUTION``: A long-running operation timed out (exceeded ``timeout`` for command execution, watch, etc.). + """ + + SANDBOX = "sandbox" + REQUEST = "request" + EXECUTION = "execution" + + def format_sandbox_timeout_exception(message: str): return TimeoutException( - f"{message}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeout' when starting the sandbox or calling '.set_timeout' on the sandbox with the desired timeout." + f"{message}: This error is likely due to sandbox timeout. You can modify the sandbox timeout by passing 'timeout' when starting the sandbox or calling '.set_timeout' on the sandbox with the desired timeout.", + type=TimeoutType.SANDBOX, ) def format_request_timeout_error() -> Exception: return TimeoutException( "Request timed out — the 'request_timeout' option can be used to increase this timeout", + type=TimeoutType.REQUEST, ) def format_execution_timeout_error() -> Exception: return TimeoutException( "Execution timed out — the 'timeout' option can be used to increase this timeout", + type=TimeoutType.EXECUTION, ) @@ -30,13 +50,16 @@ class TimeoutException(SandboxException): """ Raised when a timeout occurs. - The `unavailable` exception type is caused by sandbox timeout.\n - The `canceled` exception type is caused by exceeding request timeout.\n - The `deadline_exceeded` exception type is caused by exceeding the timeout for process, watch, etc.\n - The `unknown` exception type is sometimes caused by the sandbox timeout when the request is not processed correctly.\n + The ``type`` attribute indicates the kind of timeout: + + - :attr:`TimeoutType.SANDBOX` — the sandbox itself timed out (idle timeout, etc.). + - :attr:`TimeoutType.REQUEST` — the HTTP request exceeded ``request_timeout``. + - :attr:`TimeoutType.EXECUTION` — a long-running operation exceeded its ``timeout``. """ - pass + def __init__(self, message: str, type: TimeoutType = TimeoutType.SANDBOX): + super().__init__(message) + self.type = type class InvalidArgumentException(SandboxException): From f7d5eb07b85e182041f054e4dd89e646b170d7fc Mon Sep 17 00:00:00 2001 From: Maxwell Calkin <101308415+MaxwellCalkin@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:11:05 -0400 Subject: [PATCH 2/2] fix: make TimeoutException message parameter optional Preserve backward compatibility by defaulting message to "Timeout" so that TimeoutException() with no arguments still works. This prevents TypeError in downstream code that constructs the exception without a message. Addresses review feedback from Codex bot. AI Disclosure: This commit was authored by Claude Opus 4.6 (Anthropic), an AI agent operated by Maxwell Calkin (@MaxwellCalkin). --- packages/python-sdk/e2b/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/python-sdk/e2b/exceptions.py b/packages/python-sdk/e2b/exceptions.py index c0b1a99ce9..963a866f61 100644 --- a/packages/python-sdk/e2b/exceptions.py +++ b/packages/python-sdk/e2b/exceptions.py @@ -57,7 +57,7 @@ class TimeoutException(SandboxException): - :attr:`TimeoutType.EXECUTION` — a long-running operation exceeded its ``timeout``. """ - def __init__(self, message: str, type: TimeoutType = TimeoutType.SANDBOX): + def __init__(self, message: str = "Timeout", type: TimeoutType = TimeoutType.SANDBOX): super().__init__(message) self.type = type