## Summary When a sandbox is killed, paused, or times out while a command is streaming output, the Python SDK surfaces a raw `httpcore.RemoteProtocolError` instead of a meaningful `SandboxException`. The user gets an opaque transport-layer exception with no indication that the sandbox lifecycle is the cause. **Observed frequency:** ~3/52 965 command requests (0.006%) — low but real, and the error is confusing. ## Root cause The E2B orchestrator intentionally closes backend connections with `SetLinger(0)` (TCP RST) when a sandbox ends, to prevent connection reuse across sandbox lifecycle boundaries. This is by design in the infra. The RST propagates through the GCP load balancer, which then closes the downstream TLS connection **without sending a TLS `close_notify` alert**. On the client side, `rustls` (used by the aenv component) raises: ``` peer closed connection without sending TLS close_notify ``` `httpcore` wraps this as `httpcore.RemoteProtocolError` and it bubbles up through the SDK unhandled. ## Error propagation path in the SDK ``` Commands._start() → ProcessClient.start() → connect.Client.call_server_stream() # e2b_connect/client.py → for chunk in http_resp.iter_stream(): # ← RemoteProtocolError raised here yield parsed → CommandHandle._handle_events() → for event in self._events: # iterates the generator above ... except Exception as e: raise handle_rpc_exception(e) # e2b/envd/rpc.py ``` **Problem 1 — `handle_rpc_exception` passes `RemoteProtocolError` through unchanged:** ```python def handle_rpc_exception(e, error_map=None): if isinstance(e, ConnectException): ... # only handles ConnectRPC protocol errors else: return e # ← RemoteProtocolError returned as-is, no mapping ``` **Problem 2 — `@_retry(RemoteProtocolError, 3)` on `call_server_stream` is a no-op for streaming:** The decorator wraps a generator function. `return func(*args, **kwargs)` returns the generator object immediately without executing any body. The exception only occurs during iteration (inside `_handle_events`), outside the decorator's `try/except`. So the retry never fires during streaming. **Problem 3 — `wait()` raises a generic `Exception` when the stream ends without an end event:** ```python if self._result is None: raise Exception("Command ended without an end event") # not SandboxException ``` ## Suggested fix **`e2b_connect/client.py`** — catch TLS EOF in the body-reading loop and treat it as clean stream end. The ConnectRPC envelope framing (`FLAG_END`) is the authoritative end-of-stream signal; TLS `close_notify` is not required: ```python def _is_tls_eof(exc): msg = str(exc).lower() if isinstance(exc, RemoteProtocolError): return "close_notify" in msg or "unexpected eof" in msg if isinstance(exc, ssl.SSLEOFError): return True cause = exc.__cause__ or exc.__context__ return cause is not None and cause is not exc and _is_tls_eof(cause) # In call_server_stream / acall_server_stream: try: for chunk in http_resp.iter_stream(): for parsed in parser.parse(chunk): yield parsed except Exception as exc: if _is_tls_eof(exc): return # sandbox ended; caller surfaces a clear error if no end event was received raise ``` **`e2b/envd/rpc.py`** — map transport EOF to a readable `SandboxException`: ```python def handle_rpc_exception(e, error_map=None): if isinstance(e, ConnectException): ... # existing logic if _is_transport_eof(e): return SandboxException( "Sandbox connection closed unexpectedly. " "The sandbox may have been killed, paused, or timed out. " f"Original error: {e}" ) return e ``` **`command_handle.py` (sync + async)** — use `SandboxException` instead of bare `Exception`: ```python if self._result is None: raise SandboxException( "Command stream ended without an exit event. " "The sandbox may have been killed, paused, or timed out." ) ``` ## Files to change | File | Change | |------|--------| | `packages/python-sdk/e2b_connect/client.py` | Catch TLS EOF in `call_server_stream` + `acall_server_stream` body loops | | `packages/python-sdk/e2b/envd/rpc.py` | Map transport EOF → `SandboxException` in `handle_rpc_exception` | | `packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py` | `SandboxException` instead of bare `Exception` in `wait()` | | `packages/python-sdk/e2b/sandbox_async/commands/command_handle.py` | Same |