fix: BUG-04 webhook JSON Content-Type, BUG-05 obdJson list guard #13

Open
kianiadee wants to merge 1 commit from fix/bugs-04-05 into main

View file

@ -102,25 +102,60 @@ def _parse_data_list(raw: str) -> list[dict]:
async def _parse_request(request: Request) -> tuple[str, list[dict]]:
"""Extract token + data_list from either a JSON body or form-encoded body.
Jimi's integration push API sends:
Jimi's integration push has been observed to use form-encoding in
production:
Content-Type: application/x-www-form-urlencoded
Body: msgType=<topic>&data=<URL-encoded JSON object or array>
However the Jimi documentation describes a JSON variant:
Content-Type: application/json
Body: {"token": "...", "data_list": [{...}, ...]}
Some older/configured endpoints may still use form-encoded. This helper
handles both so each endpoint doesn't need to know which format arrived.
[BUG-04] Branch on Content-Type so a JSON push is not silently dropped
by `request.form()` returning empty. Form path remains the default
fallback when the header is missing or unrecognised, matching the live
Jimi behaviour.
"""
body = await request.body()
if not body:
return "", []
# Jimi integration push format (observed live):
# Content-Type: application/x-www-form-urlencoded
# Body: msgType=<topic>&data=<URL-encoded JSON object or array>
content_type = (request.headers.get("content-type") or "").lower()
# JSON path: parse body as a single JSON document with {token, data_list|data}.
if content_type.startswith("application/json"):
try:
payload = json.loads(body)
except (json.JSONDecodeError, TypeError):
log.warning("push: JSON body parse failed — %.200s", body[:200])
return "", []
if not isinstance(payload, dict):
log.warning("push: JSON body is not an object — got %s", type(payload).__name__)
return "", []
token = str(payload.get("token", ""))
raw_data = payload.get("data_list") if payload.get("data_list") is not None else payload.get("data")
if raw_data is None:
return token, []
# data_list / data may itself be a JSON string OR a parsed list/object.
if isinstance(raw_data, str):
try:
raw_data = json.loads(raw_data)
except (json.JSONDecodeError, TypeError):
log.warning("push: JSON data field parse failed — %.200s", raw_data)
return token, []
items = raw_data if isinstance(raw_data, list) else [raw_data]
if len(items) > MAX_ITEMS_PER_POST:
log.warning("push: truncated %d%d items", len(items), MAX_ITEMS_PER_POST)
items = items[:MAX_ITEMS_PER_POST]
return token, items
# Form-encoded path (observed live):
# msgType=<topic>&data=<URL-encoded JSON object or array>
# The `data` field holds a single JSON object per event, not an array.
try:
form = await request.form()
except Exception:
log.warning("push: form parse failed", exc_info=True)
log.warning("push: form parse failed (content-type=%s)", content_type, exc_info=True)
return "", []
token = str(form.get("token", ""))
@ -225,6 +260,14 @@ async def push_obd(request: Request):
obd = json.loads(obd)
except json.JSONDecodeError:
obd = {}
# [BUG-05] obdJson may parse to a list ([{...}]) on some
# devices. Without this guard the later obd.get() raises
# AttributeError and the item is silently dropped by the
# outer except. Take the first dict element when present.
if isinstance(obd, list):
obd = next((e for e in obd if isinstance(e, dict)), {})
if not isinstance(obd, dict):
obd = {}
# [BUG-01] Try unix epoch first, fall back to ISO string.
event_time = (