langchain-ai/langgraph · error · RuntimeError

Protocol transport is closed.

Error message

Protocol transport is closed.

What it means

Lifecycle guard on the async HTTP protocol transport: send_command (and other operations) check the _closed flag set by close(). The error means a command was posted after the transport was explicitly closed, e.g. sending run.start or join on a stream session that was already torn down.

Solutions

  1. Open a new stream session (new transport) instead of reusing one that has been closed
  2. Ensure you do not call close() (or exit the context manager) before issuing further commands
  3. Structure code so all commands are sent within the stream's lifetime (inside the 'with'/'async with' block)
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at libs/sdk-py/langgraph_sdk/stream/transport/http.py:73 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of langchain-ai/langgraph@38031739e5 (2026-08-26). Data as JSON: /api/errors/95480f05febce7ff. Report an issue: GitHub.

Appendix: source

Thrown at libs/sdk-py/langgraph_sdk/stream/transport/http.py:73

        )
        self._stream_url = (
            stream_path or f"/threads/{_quote_path_param(thread_id)}/stream/events"
        )
        self._default_headers: dict[str, str] = dict(headers or {})
        self._max_queue_size = max_queue_size
        self._closed = False
        self._event_streams: set[asyncio.Task[None]] = set()

    async def send_command(self, command: dict[str, Any]) -> dict[str, Any] | None:
        """POST a command. Returns the response JSON, or `None` for 202/204.

        Raises:
            httpx.HTTPStatusError: server returned >= 400.
            RuntimeError: the transport has been closed via `close()`.
            RuntimeError: server returned a response missing the protocol envelope.
        """
        if self._closed:
            raise RuntimeError("Protocol transport is closed.")
        # Merge default headers first so content-type always wins.
        merged_headers = {**self._default_headers, "content-type": "application/json"}
        response = await self._client.post(
            self._commands_url,
            content=orjson.dumps(command),
            headers=merged_headers,
        )
        response.raise_for_status()
        if response.status_code in (202, 204):
            return None
        if not response.content:
            raise RuntimeError("Protocol command did not return a valid response.")
        try:
            payload = orjson.loads(response.content)
        except orjson.JSONDecodeError as err:
            raise RuntimeError(
                "Protocol command did not return a valid response."
            ) from err

View on GitHub (pinned to 38031739e5)