mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
fix(ume): accept empty JSON body on successful delete-subscription
Parse 204/empty UME responses without JSONDecodeError; improve request error messages and orphan-delete failure reporting. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a8b359f60d
commit
635cd52d4f
3 changed files with 64 additions and 8 deletions
|
|
@ -162,7 +162,12 @@ def establish_alarm_subscription_manual(client: UMEClient, db: Session) -> dict[
|
||||||
if not orphan_id:
|
if not orphan_id:
|
||||||
raise
|
raise
|
||||||
_ws_log.warning("establish: UME reports existing subscription id=%s, deleting then retry", orphan_id)
|
_ws_log.warning("establish: UME reports existing subscription id=%s, deleting then retry", orphan_id)
|
||||||
_delete_subscription_on_ume(client, orphan_id)
|
try:
|
||||||
|
_delete_subscription_on_ume(client, orphan_id)
|
||||||
|
except RuntimeError as del_exc:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"ume_orphan_subscription_delete_failed:id={orphan_id}:{str(del_exc)[:180]}"
|
||||||
|
) from del_exc
|
||||||
sub_id, uri = client.establish_alarm_subscription(topic=topic)
|
sub_id, uri = client.establish_alarm_subscription(topic=topic)
|
||||||
save_subscription(db, subscription_id=sub_id, wss_uri=uri, topic=topic)
|
save_subscription(db, subscription_id=sub_id, wss_uri=uri, topic=topic)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,21 @@ def _coerce_list(value: Any) -> list[Any]:
|
||||||
return value if isinstance(value, list) else []
|
return value if isinstance(value, list) else []
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_json_response(resp: httpx.Response) -> dict[str, Any]:
|
||||||
|
"""Parse JSON body; UME often returns 204/empty body on successful DELETE operations."""
|
||||||
|
raw = str(resp.text or "").strip()
|
||||||
|
if not raw:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
return _coerce_dict(resp.json())
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
if resp.is_success:
|
||||||
|
return {}
|
||||||
|
raise RuntimeError(
|
||||||
|
f"ume_request_failed:{int(resp.status_code)}:non_json_body:{raw[:200]}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RequestDiagnostics:
|
class RequestDiagnostics:
|
||||||
method: str
|
method: str
|
||||||
|
|
@ -422,8 +437,9 @@ class UMEClient:
|
||||||
marker=marker,
|
marker=marker,
|
||||||
is_end_of_reply=is_end_of_reply,
|
is_end_of_reply=is_end_of_reply,
|
||||||
)
|
)
|
||||||
raise RuntimeError(f"ume_request_failed:{resp.status_code}:{resp.text[:240]}")
|
body_snip = raw[:240] if (raw := str(resp.text or "").strip()) else "(empty body)"
|
||||||
data = _coerce_dict(resp.json())
|
raise RuntimeError(f"ume_request_failed:{m} {path} {resp.status_code}:{body_snip}")
|
||||||
|
data = _parse_json_response(resp)
|
||||||
diag = RequestDiagnostics(
|
diag = RequestDiagnostics(
|
||||||
method=m,
|
method=m,
|
||||||
path=path,
|
path=path,
|
||||||
|
|
@ -437,7 +453,7 @@ class UMEClient:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if isinstance(exc, RuntimeError):
|
if isinstance(exc, RuntimeError):
|
||||||
raise
|
raise
|
||||||
raise RuntimeError(f"ume_request_failed:{str(exc)[:240]}") from exc
|
raise RuntimeError(f"ume_request_failed:{m} {path}:{str(exc)[:200]}") from exc
|
||||||
|
|
||||||
def request_json(
|
def request_json(
|
||||||
self,
|
self,
|
||||||
|
|
@ -477,8 +493,9 @@ class UMEClient:
|
||||||
marker=marker,
|
marker=marker,
|
||||||
is_end_of_reply=is_end_of_reply,
|
is_end_of_reply=is_end_of_reply,
|
||||||
)
|
)
|
||||||
raise RuntimeError(f"ume_request_failed:{resp.status_code}:{resp.text[:240]}")
|
body_snip = raw[:240] if (raw := str(resp.text or "").strip()) else "(empty body)"
|
||||||
data = _coerce_dict(resp.json())
|
raise RuntimeError(f"ume_request_failed:{m} {path} {resp.status_code}:{body_snip}")
|
||||||
|
data = _parse_json_response(resp)
|
||||||
diag = RequestDiagnostics(
|
diag = RequestDiagnostics(
|
||||||
method=m,
|
method=m,
|
||||||
path=path,
|
path=path,
|
||||||
|
|
@ -492,7 +509,7 @@ class UMEClient:
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if isinstance(exc, RuntimeError):
|
if isinstance(exc, RuntimeError):
|
||||||
raise
|
raise
|
||||||
raise RuntimeError(f"ume_request_failed:{str(exc)[:240]}") from exc
|
raise RuntimeError(f"ume_request_failed:{m} {path}:{str(exc)[:200]}") from exc
|
||||||
|
|
||||||
def _extract_named_list(self, payload: dict[str, Any], keys: list[str]) -> list[dict[str, Any]]:
|
def _extract_named_list(self, payload: dict[str, Any], keys: list[str]) -> list[dict[str, Any]]:
|
||||||
target_keys = {str(k).lower() for k in keys}
|
target_keys = {str(k).lower() for k in keys}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
import unittest
|
import unittest
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
@ -10,7 +11,7 @@ from sqlalchemy.orm import sessionmaker
|
||||||
from netx_api.db import Base
|
from netx_api.db import Base
|
||||||
from netx_api.main import _extract_ume_raw_group_field, _serialize_ume_alarm_raw_row, sql_ume_query, ume_alarms_fields
|
from netx_api.main import _extract_ume_raw_group_field, _serialize_ume_alarm_raw_row, sql_ume_query, ume_alarms_fields
|
||||||
from netx_api.models import UmeAlarmCurrent, UmeInventoryNE
|
from netx_api.models import UmeAlarmCurrent, UmeInventoryNE
|
||||||
from netx_api.ume_client import UMEClient
|
from netx_api.ume_client import UMEClient, _parse_json_response
|
||||||
from netx_api import ume_alarm_ws
|
from netx_api import ume_alarm_ws
|
||||||
from netx_api.models import UmeAlarmSubscription
|
from netx_api.models import UmeAlarmSubscription
|
||||||
from netx_api.ume_alarm_subscription_store import clear_subscription, load_subscription, save_subscription
|
from netx_api.ume_alarm_subscription_store import clear_subscription, load_subscription, save_subscription
|
||||||
|
|
@ -175,6 +176,39 @@ class UMEClientTests(unittest.TestCase):
|
||||||
self.assertEqual(sub_id, "3282ac78-b38a-4242-81d2-cc5b77c28ef8")
|
self.assertEqual(sub_id, "3282ac78-b38a-4242-81d2-cc5b77c28ef8")
|
||||||
self.assertIn("wss://", uri)
|
self.assertIn("wss://", uri)
|
||||||
|
|
||||||
|
def test_parse_json_response_accepts_empty_success_body(self):
|
||||||
|
class _Resp:
|
||||||
|
is_success = True
|
||||||
|
status_code = 204
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
raise json.JSONDecodeError("Expecting value", "", 0)
|
||||||
|
|
||||||
|
self.assertEqual(_parse_json_response(_Resp()), {}) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
def test_delete_alarm_subscription_empty_response(self):
|
||||||
|
from time import time as _time
|
||||||
|
|
||||||
|
client = UMEClient(base_url="https://ume.local:18014", username="u", password="p", verify_tls=False)
|
||||||
|
client._token_value = "token-1"
|
||||||
|
client._token_expires_at = _time() + 3600
|
||||||
|
|
||||||
|
class _Resp:
|
||||||
|
is_success = True
|
||||||
|
status_code = 204
|
||||||
|
text = ""
|
||||||
|
headers: dict[str, str] = {}
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
raise json.JSONDecodeError("Expecting value", "", 0)
|
||||||
|
|
||||||
|
def _fake_request(method: str, path: str, *, params=None, body=None):
|
||||||
|
return _parse_json_response(_Resp()), None # type: ignore[arg-type]
|
||||||
|
|
||||||
|
client.request_json = _fake_request # type: ignore[method-assign]
|
||||||
|
client.delete_alarm_subscription("sub-to-delete")
|
||||||
|
|
||||||
def test_delete_alarm_subscription(self):
|
def test_delete_alarm_subscription(self):
|
||||||
from time import time as _time
|
from time import time as _time
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue