From 378682bd57ec78d7db0b3cac835cd33c7d2b8eaa Mon Sep 17 00:00:00 2001 From: david kiania Date: Fri, 15 May 2026 15:42:17 +0300 Subject: [PATCH] fix: BUG-04 honour JSON Content-Type in webhook _parse_request, BUG-05 guard obdJson list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- webhook_receiver_rev.py | 57 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/webhook_receiver_rev.py b/webhook_receiver_rev.py index b0d7108..492710e 100644 --- a/webhook_receiver_rev.py +++ b/webhook_receiver_rev.py @@ -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=&data= + + 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=&data= + 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=&data= # 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 = ( -- 2.45.2