fix: handle JSON body push format from Jimi integration API
Jimi's integration push API (tracksolidprodocs.jimicloud.com) sends
Content-Type: application/json with body {"token":"...","data_list":[...]},
not form-encoded. FastAPI Form() silently defaulted to "" so all pushes
were discarded with "Failed to parse data_list:" warnings.
Replaces per-endpoint Form() params with a shared _parse_request() helper
that tries JSON body first, falls back to form-encoded. All seven push
endpoints (pushobd, pushfaultinfo, pushalarm, pushgps, pushhb,
pushtripreport, pushevent) updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
85d02c81a5
commit
ef36ebebea
1 changed files with 66 additions and 17 deletions
|
|
@ -41,7 +41,7 @@ from typing import Optional
|
||||||
# monopolising a worker or blowing the DB pool. Jimi normally sends ≤ 200.
|
# monopolising a worker or blowing the DB pool. Jimi normally sends ≤ 200.
|
||||||
MAX_ITEMS_PER_POST = int(os.getenv("WEBHOOK_MAX_ITEMS", "5000"))
|
MAX_ITEMS_PER_POST = int(os.getenv("WEBHOOK_MAX_ITEMS", "5000"))
|
||||||
|
|
||||||
from fastapi import FastAPI, Form, HTTPException
|
from fastapi import FastAPI, HTTPException, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from psycopg2.extras import execute_values
|
from psycopg2.extras import execute_values
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ def _validate_token(token: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def _parse_data_list(raw: str) -> list[dict]:
|
def _parse_data_list(raw: str) -> list[dict]:
|
||||||
"""Parse the JSON string from Jimi's data_list form field."""
|
"""Parse a JSON string into a list of dicts. raw may be a JSON array or single object."""
|
||||||
try:
|
try:
|
||||||
parsed = json.loads(raw)
|
parsed = json.loads(raw)
|
||||||
items = parsed if isinstance(parsed, list) else [parsed]
|
items = parsed if isinstance(parsed, list) else [parsed]
|
||||||
|
|
@ -96,10 +96,59 @@ def _parse_data_list(raw: str) -> list[dict]:
|
||||||
items = items[:MAX_ITEMS_PER_POST]
|
items = items[:MAX_ITEMS_PER_POST]
|
||||||
return items
|
return items
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
log.warning("Failed to parse data_list: %.200s", raw)
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
content_type = request.headers.get("content-type", "")
|
||||||
|
body = await request.body()
|
||||||
|
|
||||||
|
if not body:
|
||||||
|
log.debug("push: empty request body")
|
||||||
|
return "", []
|
||||||
|
|
||||||
|
# ── Try JSON body first (integration push format) ──────────────────────────
|
||||||
|
if "application/json" in content_type or body.lstrip()[:1] == b"{":
|
||||||
|
try:
|
||||||
|
payload = json.loads(body)
|
||||||
|
token = str(payload.get("token", ""))
|
||||||
|
raw_dl = payload.get("data_list", [])
|
||||||
|
if isinstance(raw_dl, list):
|
||||||
|
items = raw_dl[:MAX_ITEMS_PER_POST]
|
||||||
|
elif isinstance(raw_dl, str):
|
||||||
|
items = _parse_data_list(raw_dl)
|
||||||
|
else:
|
||||||
|
items = []
|
||||||
|
log.debug("push: parsed JSON body — %d items", len(items))
|
||||||
|
return token, items
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
log.warning("push: JSON body parse failed — body: %.200s",
|
||||||
|
body.decode("utf-8", errors="replace"))
|
||||||
|
|
||||||
|
# ── Fall back to form-encoded ───────────────────────────────────────────────
|
||||||
|
try:
|
||||||
|
form = await request.form()
|
||||||
|
token = str(form.get("token", ""))
|
||||||
|
raw_dl = str(form.get("data_list", ""))
|
||||||
|
items = _parse_data_list(raw_dl) if raw_dl else []
|
||||||
|
log.debug("push: parsed form body — %d items", len(items))
|
||||||
|
return token, items
|
||||||
|
except Exception:
|
||||||
|
log.warning("push: form body parse failed — body: %.200s",
|
||||||
|
body.decode("utf-8", errors="replace"), exc_info=True)
|
||||||
|
|
||||||
|
return "", []
|
||||||
|
|
||||||
|
|
||||||
def unix_to_ts(v) -> Optional[str]:
|
def unix_to_ts(v) -> Optional[str]:
|
||||||
"""Convert Unix timestamp (seconds or milliseconds) to ISO string."""
|
"""Convert Unix timestamp (seconds or milliseconds) to ISO string."""
|
||||||
if v is None:
|
if v is None:
|
||||||
|
|
@ -145,9 +194,9 @@ def health():
|
||||||
# ── 1. OBD Diagnostics (Priority 1) ──────────────────────────────────────────
|
# ── 1. OBD Diagnostics (Priority 1) ──────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/pushobd")
|
@app.post("/pushobd")
|
||||||
def push_obd(token: str = Form(""), data_list: str = Form("")):
|
async def push_obd(request: Request):
|
||||||
|
token, items = await _parse_request(request)
|
||||||
_validate_token(token)
|
_validate_token(token)
|
||||||
items = _parse_data_list(data_list)
|
|
||||||
if not items:
|
if not items:
|
||||||
return JSONResponse(content=SUCCESS)
|
return JSONResponse(content=SUCCESS)
|
||||||
|
|
||||||
|
|
@ -216,9 +265,9 @@ def push_obd(token: str = Form(""), data_list: str = Form("")):
|
||||||
# ── 2. DTC Fault Codes (Priority 1) ──────────────────────────────────────────
|
# ── 2. DTC Fault Codes (Priority 1) ──────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/pushfaultinfo")
|
@app.post("/pushfaultinfo")
|
||||||
def push_fault_info(token: str = Form(""), data_list: str = Form("")):
|
async def push_fault_info(request: Request):
|
||||||
|
token, items = await _parse_request(request)
|
||||||
_validate_token(token)
|
_validate_token(token)
|
||||||
items = _parse_data_list(data_list)
|
|
||||||
if not items:
|
if not items:
|
||||||
return JSONResponse(content=SUCCESS)
|
return JSONResponse(content=SUCCESS)
|
||||||
|
|
||||||
|
|
@ -286,9 +335,9 @@ def push_fault_info(token: str = Form(""), data_list: str = Form("")):
|
||||||
# ── 3. Alarm Events (Priority 2) ─────────────────────────────────────────────
|
# ── 3. Alarm Events (Priority 2) ─────────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/pushalarm")
|
@app.post("/pushalarm")
|
||||||
def push_alarm(token: str = Form(""), data_list: str = Form("")):
|
async def push_alarm(request: Request):
|
||||||
|
token, items = await _parse_request(request)
|
||||||
_validate_token(token)
|
_validate_token(token)
|
||||||
items = _parse_data_list(data_list)
|
|
||||||
if not items:
|
if not items:
|
||||||
return JSONResponse(content=SUCCESS)
|
return JSONResponse(content=SUCCESS)
|
||||||
|
|
||||||
|
|
@ -343,9 +392,9 @@ def push_alarm(token: str = Form(""), data_list: str = Form("")):
|
||||||
# ── 4. GPS Positions (Priority 2) ────────────────────────────────────────────
|
# ── 4. GPS Positions (Priority 2) ────────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/pushgps")
|
@app.post("/pushgps")
|
||||||
def push_gps(token: str = Form(""), data_list: str = Form("")):
|
async def push_gps(request: Request):
|
||||||
|
token, items = await _parse_request(request)
|
||||||
_validate_token(token)
|
_validate_token(token)
|
||||||
items = _parse_data_list(data_list)
|
|
||||||
if not items:
|
if not items:
|
||||||
return JSONResponse(content=SUCCESS)
|
return JSONResponse(content=SUCCESS)
|
||||||
|
|
||||||
|
|
@ -407,9 +456,9 @@ def push_gps(token: str = Form(""), data_list: str = Form("")):
|
||||||
# ── 5. Device Heartbeats (Priority 2) ────────────────────────────────────────
|
# ── 5. Device Heartbeats (Priority 2) ────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/pushhb")
|
@app.post("/pushhb")
|
||||||
def push_heartbeat(token: str = Form(""), data_list: str = Form("")):
|
async def push_heartbeat(request: Request):
|
||||||
|
token, items = await _parse_request(request)
|
||||||
_validate_token(token)
|
_validate_token(token)
|
||||||
items = _parse_data_list(data_list)
|
|
||||||
if not items:
|
if not items:
|
||||||
return JSONResponse(content=SUCCESS)
|
return JSONResponse(content=SUCCESS)
|
||||||
|
|
||||||
|
|
@ -456,9 +505,9 @@ def push_heartbeat(token: str = Form(""), data_list: str = Form("")):
|
||||||
# ── 6. Trip Reports (Priority 2) ─────────────────────────────────────────────
|
# ── 6. Trip Reports (Priority 2) ─────────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/pushtripreport")
|
@app.post("/pushtripreport")
|
||||||
def push_trip_report(token: str = Form(""), data_list: str = Form("")):
|
async def push_trip_report(request: Request):
|
||||||
|
token, items = await _parse_request(request)
|
||||||
_validate_token(token)
|
_validate_token(token)
|
||||||
items = _parse_data_list(data_list)
|
|
||||||
if not items:
|
if not items:
|
||||||
return JSONResponse(content=SUCCESS)
|
return JSONResponse(content=SUCCESS)
|
||||||
|
|
||||||
|
|
@ -531,9 +580,9 @@ def push_trip_report(token: str = Form(""), data_list: str = Form("")):
|
||||||
# ── 7. Device Events (LOGIN / LOGOUT) ────────────────────────────────────────
|
# ── 7. Device Events (LOGIN / LOGOUT) ────────────────────────────────────────
|
||||||
|
|
||||||
@app.post("/pushevent")
|
@app.post("/pushevent")
|
||||||
def push_event(token: str = Form(""), data_list: str = Form("")):
|
async def push_event(request: Request):
|
||||||
|
token, items = await _parse_request(request)
|
||||||
_validate_token(token)
|
_validate_token(token)
|
||||||
items = _parse_data_list(data_list)
|
|
||||||
if not items:
|
if not items:
|
||||||
return JSONResponse(content=SUCCESS)
|
return JSONResponse(content=SUCCESS)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue