fix(api): parse form-urlencoded POST body in fleet-dashboard handler
Some checks are pending
Static Analysis / static (push) Waiting to run
Tests / test (push) Waiting to run

The Fleet Trips SPA posts application/x-www-form-urlencoded, but the
POST /webhook/fleet-dashboard handler read the body with request.json().
That threw on every request, the except swallowed it to body={}, and all
filters (vehicle_numbers, cost_centre, assigned_city) plus period/dates
were dropped — so every query returned the full unfiltered fleet (1,266
trips) regardless of the dropdowns. The map/KPIs/trips never changed,
which read as "the dropdowns don't work."

Parse by Content-Type: urllib.parse.parse_qs for form bodies (no new
dependency — avoids python-multipart), JSON still accepted defensively
for n8n-compat callers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
david kiania 2026-06-05 13:23:10 +03:00
parent 26fa1a4dc5
commit f1387d1476

View file

@ -31,9 +31,11 @@ is the base URL (the `N8N_BASE` constant in each dashboard SPA):
from __future__ import annotations from __future__ import annotations
import json
import os import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import date, datetime, timedelta, timezone from datetime import date, datetime, timedelta, timezone
from urllib.parse import parse_qs
import psycopg2.extras import psycopg2.extras
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
@ -202,13 +204,23 @@ def _preset_to_range(period: str | None, start_date, end_date):
@app.post("/webhook/fleet-dashboard") @app.post("/webhook/fleet-dashboard")
async def fleet_trips(request: Request): async def fleet_trips(request: Request):
# The dashboard SPA posts application/x-www-form-urlencoded (not JSON), so
# parse by content-type. Reading the raw body + parse_qs avoids pulling in
# python-multipart. JSON is still accepted defensively (n8n-compat callers).
body: dict = {}
ctype = request.headers.get("content-type", "").lower()
try: try:
body = await request.json() raw = await request.body()
if "application/json" in ctype:
parsed = json.loads(raw or b"{}")
body = parsed if isinstance(parsed, dict) else {}
else:
# x-www-form-urlencoded — parse_qs yields lists; keep the last value.
body = {k: v[-1] for k, v in parse_qs(raw.decode("utf-8", "replace")).items()}
except Exception: except Exception:
body = {} body = {}
if not isinstance(body, dict): if isinstance(body.get("body"), dict):
body = {} body = body["body"]
body = body.get("body", body) if isinstance(body.get("body"), dict) else body
start, end = _preset_to_range( start, end = _preset_to_range(
body.get("period"), body.get("start_date"), body.get("end_date") body.get("period"), body.get("start_date"), body.get("end_date")