fix: BUG-04 honour JSON Content-Type in webhook _parse_request, BUG-05 guard obdJson list
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:
parent
dc6404a114
commit
378682bd57
1 changed files with 50 additions and 7 deletions
|
|
@ -102,25 +102,60 @@ def _parse_data_list(raw: str) -> list[dict]:
|
||||||
async def _parse_request(request: Request) -> tuple[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.
|
"""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
|
Content-Type: application/json
|
||||||
Body: {"token": "...", "data_list": [{...}, ...]}
|
Body: {"token": "...", "data_list": [{...}, ...]}
|
||||||
|
|
||||||
Some older/configured endpoints may still use form-encoded. This helper
|
[BUG-04] Branch on Content-Type so a JSON push is not silently dropped
|
||||||
handles both so each endpoint doesn't need to know which format arrived.
|
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()
|
body = await request.body()
|
||||||
if not body:
|
if not body:
|
||||||
return "", []
|
return "", []
|
||||||
|
|
||||||
# Jimi integration push format (observed live):
|
content_type = (request.headers.get("content-type") or "").lower()
|
||||||
# Content-Type: application/x-www-form-urlencoded
|
|
||||||
# Body: msgType=<topic>&data=<URL-encoded JSON object or array>
|
# 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.
|
# The `data` field holds a single JSON object per event, not an array.
|
||||||
try:
|
try:
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
except Exception:
|
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 "", []
|
return "", []
|
||||||
|
|
||||||
token = str(form.get("token", ""))
|
token = str(form.get("token", ""))
|
||||||
|
|
@ -225,6 +260,14 @@ async def push_obd(request: Request):
|
||||||
obd = json.loads(obd)
|
obd = json.loads(obd)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
obd = {}
|
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.
|
# [BUG-01] Try unix epoch first, fall back to ISO string.
|
||||||
event_time = (
|
event_time = (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue