SslAesTransport: handshake again when a camera answers 401 to passthrough (#1771)
Some checks failed
CI / Perform Lint Checks (3.14) (push) Has been cancelled
CI / Python 3.11 on macos-latest (push) Has been cancelled
CI / Python 3.12 on macos-latest (push) Has been cancelled
CI / Python 3.13 on macos-latest (push) Has been cancelled
CI / Python 3.14 on macos-latest (push) Has been cancelled
CI / Python 3.11 on ubuntu-latest (push) Has been cancelled
CI / Python 3.12 on ubuntu-latest (push) Has been cancelled
CI / Python 3.13 on ubuntu-latest (push) Has been cancelled
CI / Python 3.14 on ubuntu-latest (push) Has been cancelled
CI / Python 3.11 on windows-latest (push) Has been cancelled
CI / Python 3.12 on windows-latest (push) Has been cancelled
CI / Python 3.13 on windows-latest (push) Has been cancelled
CI / Python 3.14 on windows-latest (push) Has been cancelled
CodeQL Checks / Analyze (python) (push) Has been cancelled
Stale / stale (push) Has been cancelled

Some cameras (C220 and C100 on the 1.4.4 line in those reports) drop the
session about every ten minutes and answer the next `securePassthrough`
with HTTP 401 and `-40421` in the body.

With this change a 401 on passthrough marks the transport as needing a
handshake and raises `_RetryableError`, so the protocol logs in again
and retries the same request.

---------

Co-authored-by: freeKC <7538438+freeKC@users.noreply.github.com>
This commit is contained in:
FreeKC
2026-10-03 18:19:40 +02:00
committed by GitHub
parent 998ebe79e8
commit b0d77b35a2
2 changed files with 35 additions and 0 deletions

View File

@@ -274,6 +274,20 @@ class SslAesTransport(BaseTransport):
_LOGGER.debug(msg)
raise _RetryableError(msg)
# Some devices answer 401 when the session has expired and they
# require a new handshake: reauthenticate and retry the request.
if status_code == 401:
_LOGGER.debug(
"Device %s replied with status 401 to passthrough, "
"session expired, handshake required",
self._host,
)
self._state = TransportState.HANDSHAKE_REQUIRED
raise _RetryableError(
f"{self._host} responded with status 401 to passthrough, "
"session expired"
)
if status_code != 200:
raise KasaException(
f"{self._host} responded with an unexpected "

View File

@@ -797,3 +797,24 @@ class MockSslAesDevice:
def put_next_response(self, request: dict | bytes) -> None:
self._next_responses.append(request)
async def test_passthrough_401_requires_new_handshake(mocker):
"""A 401 on passthrough means the session expired: retryable, new handshake."""
host = "127.0.0.1"
mock_ssl_aes_device = MockSslAesDevice(host)
mocker.patch.object(
aiohttp.ClientSession, "post", side_effect=mock_ssl_aes_device.post
)
transport = SslAesTransport(
config=DeviceConfig(host, credentials=Credentials(MOCK_USER, MOCK_PWD))
)
request = {"method": "getDeviceInfo", "params": None}
await transport.perform_handshake()
assert transport._state is TransportState.ESTABLISHED
mock_ssl_aes_device.status_code = 401
with pytest.raises(_RetryableError, match="session expired"):
await transport.send(json_dumps(request))
assert transport._state is TransportState.HANDSHAKE_REQUIRED