fix(runtime): swallow torn-down docker socket in sandbox delete() (#721)

StrixDockerSandboxClient.delete() best-effort-kills the sandbox container via
containers.get(id).kill() before delegating to the SDK's delete(), suppressing
docker NotFound/APIError. But when the docker daemon socket is already going
away — the normal case on a host/CI teardown — containers.get() ->
inspect_container raises requests' ConnectionError, which is a *sibling* of
docker.errors.APIError under requests.RequestException, not a subclass. So it
escapes the APIError-only suppress and surfaces a full traceback on teardown
even though the kill is meant to be best-effort.

Add RequestException to the suppress so the best-effort kill is genuinely
best-effort regardless of daemon reachability.

Test: tests/test_docker_client_delete.py — the kill raising ConnectionError
(and NotFound/APIError) is swallowed and delete() still delegates; unrelated
errors still propagate; no-container_id is a no-op. The ConnectionError case
fails against the pre-fix APIError-only suppress.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seanturner83
2026-07-10 00:13:35 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent e1940769de
commit 0fb005c73f
2 changed files with 97 additions and 1 deletions
+11 -1
View File
@@ -40,6 +40,7 @@ from docker import errors as docker_errors # type: ignore[import-untyped, unuse
from docker.models.containers import Container # type: ignore[import-untyped, unused-ignore]
from docker.types import Mount as DockerSDKMount # type: ignore[import-untyped, unused-ignore]
from docker.utils import parse_repository_tag # type: ignore[import-untyped, unused-ignore]
from requests.exceptions import RequestException
logger = logging.getLogger(__name__)
@@ -148,6 +149,15 @@ class StrixDockerSandboxClient(DockerSandboxClient):
async def delete(self, session: SandboxSession) -> SandboxSession:
container_id = getattr(getattr(session._inner, "state", None), "container_id", None)
if container_id:
with contextlib.suppress(docker_errors.NotFound, docker_errors.APIError):
# Best-effort kill: NotFound/APIError cover a gone or unhappy
# container. RequestException covers a torn-down daemon socket —
# containers.get() -> inspect_container raises requests'
# ConnectionError, which is a sibling of docker.errors.APIError
# under requests.RequestException (not a subclass), so it escapes
# an APIError-only suppress and surfaces a full traceback even
# though this teardown is meant to be best-effort.
with contextlib.suppress(
docker_errors.NotFound, docker_errors.APIError, RequestException
):
self.docker_client.containers.get(container_id).kill()
return await super().delete(session)