fix(chv): api_client 接受 204 无 Content-Length 响应

WSL 真机实测(CHV 54.0.0):vm.create 成功回 204 No Content,
且不带 Content-Length、无体;严格校验(P1-6)把合法成功当
失败拒掉,wake_vm 卡在 vm.create。204/304 按 HTTP 语义体长
恒为 0,不要求长度、不读体(keep-alive 下读会等到超时);
其它状态仍严格(无长度/截断/chunked 均拒绝)。
测试:test_chv_cover100 新增 3 项(204 成功、200 无长度仍拒、
_parse_status)
This commit is contained in:
host committed 2026-11-06 17:43:54 +00:00
1 parent d7cc482ed5
commit 5b6514d019
2 files changed
+66 -3

No files matched your search

+22 -3
View File
@@ -33,6 +33,15 @@ def _read_header(sock):
return header, buf[header_end + 4:]
def _parse_status(header: str) -> int:
"""从响应状态行取状态码(解析失败即 0,与
_parse_response 的口径一致)。"""
try:
return int(header.split()[1])
except (IndexError, ValueError):
return 0
def _parse_content_length(header):
"""从响应头取 Content-Length:chunked 或无长度即拒绝。
@@ -102,7 +111,13 @@ def _unix_socket_api(socket_path, method, path, payload=None):
body 能解析 JSON 时为 dict,否则为原文本。socket 不存在
或连接失败时 raise RuntimeError(调用方据此判 api 超时
/失败,不编造 200)。读满语义见 _read_header /
_parse_content_length / _read_body(P1-6,纯移动)。"""
_parse_content_length / _read_body(P1-6,纯移动)。
204/304 例外:按 HTTP 语义体长恒为 0,不要求
Content-Length、也不读体——CHV 的 vm.create/vm.boot
成功即回 204 且不带该头(2026-11-07 WSL 真机实测:
vmm.ping 200 带长度、vm.create 204 无长度),严格
校验会把合法成功当失败拒掉;其它状态仍严格(无长度
/截断/chunked 均拒绝)。"""
if not os.path.exists(socket_path):
raise RuntimeError("api socket 不存在:%s" % socket_path)
body = b""
@@ -121,8 +136,12 @@ def _unix_socket_api(socket_path, method, path, payload=None):
sock.connect(socket_path)
sock.sendall(raw)
header, body = _read_header(sock)
content_length = _parse_content_length(header)
body = _read_body(sock, body, content_length)
if _parse_status(header) in (204, 304):
# 体长恒为 0:不读体(keep-alive 下读会等到超时)。
body = b""
else:
content_length = _parse_content_length(header)
body = _read_body(sock, body, content_length)
finally:
sock.close()
return _parse_response(header, body)
+44
View File
@@ -103,6 +103,50 @@ class ApiClientCover100Test(unittest.TestCase):
self.assertIn(b'"name": "vm1"', fake.sent)
self.assertTrue(fake.closed)
def test_204_without_content_length_is_success(self):
# 真机实测(2026-11-07,CHV 54.0.0):vm.create 成功回
# 204 No Content,且不带 Content-Length、无体——严格
# 校验会把合法成功当失败拒掉。204 按 HTTP 语义体长为
# 0,不读体(keep-alive 下读会等到超时;假 socket 的
# 剩余片为空,读体即报截断,本测试能抓住误读)。
fake = _FakeApiSocket([
b"HTTP/1.1 204 No Content\r\nServer: Cloud Hypervisor API"
b"\r\nConnection: keep-alive\r\n\r\n",
])
with tempfile.TemporaryDirectory() as tmp:
sock_path = os.path.join(tmp, "chv.sock")
with open(sock_path, "w") as fh:
fh.write("x")
with mock.patch.object(api_client.socket, "socket",
lambda *a, **k: fake):
status, body = api_client._unix_socket_api(
sock_path, "PUT", "/vm.create",
payload={"cpus": {"boot_vcpus": 2}})
self.assertEqual(status, 204)
self.assertEqual(body, {})
self.assertTrue(fake.closed)
def test_200_without_content_length_still_rejected(self):
# 204 例外不得放宽其它状态:200 无长度仍拒绝。
fake = _FakeApiSocket([
b"HTTP/1.1 200 OK\r\nContent-Type: application/json"
b"\r\n\r\n{}",
])
with tempfile.TemporaryDirectory() as tmp:
sock_path = os.path.join(tmp, "chv.sock")
with open(sock_path, "w") as fh:
fh.write("x")
with mock.patch.object(api_client.socket, "socket",
lambda *a, **k: fake):
with self.assertRaises(RuntimeError):
api_client._unix_socket_api(
sock_path, "GET", "/vmm.ping")
def test_parse_status(self):
self.assertEqual(api_client._parse_status("HTTP/1.1 204 foo"), 204)
self.assertEqual(api_client._parse_status("HTTP/1.1 OK"), 0)
self.assertEqual(api_client._parse_status(""), 0)
def test_parse_status_non_numeric_is_zero(self):
status, _body = api_client._parse_response(
"HTTP/1.1 OK\r\n\r\n", b"{}")