From f1387d14768cc89b4e19fd419a67ea4ffb4f1691 Mon Sep 17 00:00:00 2001 From: david kiania Date: Fri, 5 Jun 2026 13:23:10 +0300 Subject: [PATCH] fix(api): parse form-urlencoded POST body in fleet-dashboard handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- dashboard_api_rev.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/dashboard_api_rev.py b/dashboard_api_rev.py index 6537e83..92985c4 100644 --- a/dashboard_api_rev.py +++ b/dashboard_api_rev.py @@ -31,9 +31,11 @@ is the base URL (the `N8N_BASE` constant in each dashboard SPA): from __future__ import annotations +import json import os from contextlib import asynccontextmanager from datetime import date, datetime, timedelta, timezone +from urllib.parse import parse_qs import psycopg2.extras 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") 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: - 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: body = {} - if not isinstance(body, dict): - body = {} - body = body.get("body", body) if isinstance(body.get("body"), dict) else body + if isinstance(body.get("body"), dict): + body = body["body"] start, end = _preset_to_range( body.get("period"), body.get("start_date"), body.get("end_date")