From 84b4cee353c12fe5761e1f913998fc108f9e7327 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sat, 31 Jan 2026 08:40:39 +0000 Subject: [PATCH] Fix TypeError when Docker returns null labels in container inspect When a container has no labels, Docker returns {"Labels": null} in the inspect response. The code was using .get("Labels", {}) which only returns the default when the key is missing, not when it's null. This caused _has_webterm_label() to raise TypeError, which was silently caught by the event watcher's exception handler, causing it to reconnect and miss the container start event. Fixed by using .get("Labels") or {} pattern in: - _handle_event() when processing start events - _get_container_command() when extracting command label - _get_container_theme() when extracting theme label Added test for null labels case. --- src/webterm/docker_watcher.py | 10 ++++++---- tests/test_docker_watcher.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/webterm/docker_watcher.py b/src/webterm/docker_watcher.py index 23b92fc..2d94dfe 100644 --- a/src/webterm/docker_watcher.py +++ b/src/webterm/docker_watcher.py @@ -151,7 +151,7 @@ class DockerWatcher: If label is 'auto', empty, or missing, returns default exec command. """ - labels = container.get("Labels", {}) + labels = container.get("Labels") or {} label_value = labels.get(LABEL_NAME) if _is_auto_label(label_value): @@ -159,7 +159,7 @@ class DockerWatcher: return label_value or AUTO_COMMAND_SENTINEL def _get_container_theme(self, container: dict) -> str | None: - labels = container.get("Labels", {}) + labels = container.get("Labels") or {} value = labels.get(THEME_LABEL) if isinstance(value, str) and value.strip(): return value.strip() @@ -287,12 +287,14 @@ class DockerWatcher: if status == 200: container_info = json.loads(body) # Convert to list format expected by _add_container + # Labels can be None if container has no labels + labels = container_info.get("Config", {}).get("Labels") or {} container = { "Id": container_id, "Names": ["/" + container_info.get("Name", "").lstrip("/")], - "Labels": container_info.get("Config", {}).get("Labels", {}), + "Labels": labels, } - if _has_webterm_label(container.get("Labels", {})): + if _has_webterm_label(labels): await self._add_container(container) elif action == "die": await self._remove_container(container_id) diff --git a/tests/test_docker_watcher.py b/tests/test_docker_watcher.py index d9bb398..4f96d19 100644 --- a/tests/test_docker_watcher.py +++ b/tests/test_docker_watcher.py @@ -263,6 +263,36 @@ class TestDockerWatcherIntegration: session_manager.add_app.assert_called_once() + @pytest.mark.asyncio + async def test_handle_start_event_null_labels(self, session_manager): + """Container with null labels in Docker inspect response is handled gracefully.""" + watcher = DockerWatcher(session_manager) + + async def mock_request(method, path): + if "/containers/" in path and "/json" in path: + # Docker returns null for Labels when container has none + return ( + 200, + '{"Name": "/no-labels-container", "Config": {"Labels": null}}', + ) + return 404, "" + + watcher._docker_request = mock_request + + event = { + "Action": "start", + "Actor": { + "ID": "container456", + "Attributes": {}, + }, + } + + # Should not raise an exception + await watcher._handle_event(event) + + # Should not add container (no webterm labels) + session_manager.add_app.assert_not_called() + @pytest.mark.parametrize( ("labels", "expected"),