fix: BUG-04 honour JSON Content-Type in webhook _parse_request, BUG-05 guard obdJson list
Some checks failed
Static Analysis / static (push) Has been cancelled
Tests / test (push) Has been cancelled
Static Analysis / static (pull_request) Has been cancelled
Tests / test (pull_request) Has been cancelled

BUG-04 (MEDIUM): the _parse_request docstring promised "JSON body or
form-encoded body" support, but the implementation only called
request.form(). If Jimi sent application/json (per the docs), the form
parse returned an empty FormData, the function returned ("", []), and
the entire push was silently dropped. Now branches on Content-Type and
parses JSON bodies directly, falling back to the form path that matches
the live Jimi behaviour.

BUG-05 (MEDIUM): push_obd treated obdJson as a dict after json.loads
even though malformed payloads can decode to a list. The subsequent
obd.get(...) raised AttributeError, caught by the per-item except and
logged as a generic "Failed to process" warning — silently losing the
reading. Now coerces lists to their first dict element and falls back
to {} for any other non-dict shape so the timestamp/lat/lng extraction
still runs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
david kiania 2026-05-15 15:42:17 +03:00
parent dc6404a114
commit 378682bd57

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 = (