From d15f781803e4e0aadeddc5f808166f37e814b9e6 Mon Sep 17 00:00:00 2001 From: Rafael Benitez Date: Thu, 19 Mar 2026 11:37:09 -0300 Subject: [PATCH] fix(chart): add error logging to /api/v1/chart/data endpoint Add error-level logging to all failure paths in the chart data API to help diagnose intermittent 400 BAD REQUEST failures during CSV exports. Previously, JSON parsing errors were silently swallowed by contextlib.suppress and validation/query errors returned 400 without any logging, making it impossible to identify which failure path was hit. Co-Authored-By: Claude Opus 4.6 --- superset/charts/data/api.py | 69 ++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/superset/charts/data/api.py b/superset/charts/data/api.py index 707ce79f918..93c754c8906 100644 --- a/superset/charts/data/api.py +++ b/superset/charts/data/api.py @@ -250,9 +250,29 @@ class ChartDataRestApi(ChartRestApi): json_body = request.json elif request.form.get("form_data"): # CSV export submits regular form data - with contextlib.suppress(TypeError, json.JSONDecodeError): + try: json_body = json.loads(request.form["form_data"]) + except (TypeError, json.JSONDecodeError): + logger.error( + "Failed to parse form_data JSON: " + "content_type=%s, content_length=%s, form_data_length=%s, " + "referrer=%s", + request.content_type, + request.content_length, + len(request.form.get("form_data", "")), + request.referrer, + ) if json_body is None: + logger.error( + "Chart data request rejected: json_body is None. " + "is_json=%s, content_type=%s, content_length=%s, " + "has_form_data=%s, referrer=%s", + request.is_json, + request.content_type, + request.content_length, + bool(request.form.get("form_data")), + request.referrer, + ) return self.response_400(message=_("Request is not JSON")) try: @@ -260,10 +280,37 @@ class ChartDataRestApi(ChartRestApi): command = ChartDataCommand(query_context) command.validate() except DatasourceNotFound: + logger.error( + "Chart data request: DatasourceNotFound. " + "datasource=%s, result_format=%s, " + "slice_id=%s, referrer=%s", + json_body.get("datasource"), + json_body.get("result_format"), + json_body.get("form_data", {}).get("slice_id"), + request.referrer, + ) return self.response_404() except QueryObjectValidationError as error: + logger.error( + "Chart data request: QueryObjectValidationError: %s. " + "result_format=%s, slice_id=%s, referrer=%s", + error.message, + json_body.get("result_format"), + json_body.get("form_data", {}).get("slice_id"), + request.referrer, + ) return self.response_400(message=error.message) except ValidationError as error: + logger.error( + "Chart data request: ValidationError: %s. " + "result_format=%s, datasource=%s, " + "slice_id=%s, referrer=%s", + error.normalized_messages(), + json_body.get("result_format"), + json_body.get("datasource"), + json_body.get("form_data", {}).get("slice_id"), + request.referrer, + ) return self.response_400( message=_( "Request is incorrect: %(error)s", error=error.normalized_messages() @@ -413,9 +460,21 @@ class ChartDataRestApi(ChartRestApi): else: has_export_perm = security_manager.can_access("can_csv", "Superset") if not has_export_perm: + logger.error( + "Chart data request: export permission denied. " + "result_format=%s, referrer=%s", + result_format, + request.referrer, + ) return self.response_403() if not result["queries"]: + logger.error( + "Chart data request: empty query result. " + "result_format=%s, referrer=%s", + result_format, + request.referrer, + ) return self.response_400(_("Empty query result")) is_csv_format = result_format == ChartDataResultFormat.CSV @@ -504,6 +563,14 @@ class ChartDataRestApi(ChartRestApi): except ChartDataCacheLoadError as exc: return self.response_422(message=exc.message) except ChartDataQueryFailedError as exc: + logger.error( + "Chart data query failed: %s. " + "result_format=%s, force_cached=%s, referrer=%s", + exc.message, + form_data.get("result_format") if form_data else None, + force_cached, + request.referrer, + ) return self.response_400(message=exc.message) # Log is_cached if extra payload callback is provided