From 3d560a79f35f49a7ab70087968c53ff9a2516e84 Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Tue, 8 Sep 2026 16:13:20 +0900 Subject: [PATCH 1/2] Fix 500 for non-dict JSON bodies on variable/connection endpoints Signed-off-by: PoAn Yang --- .../airflow/api_fastapi/logging/decorators.py | 7 ++++-- .../routes/public/test_connections.py | 22 +++++++++++++++++++ .../core_api/routes/public/test_variables.py | 18 +++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/logging/decorators.py b/airflow-core/src/airflow/api_fastapi/logging/decorators.py index 3ed9dd86c79aa..64587c432a02c 100644 --- a/airflow-core/src/airflow/api_fastapi/logging/decorators.py +++ b/airflow-core/src/airflow/api_fastapi/logging/decorators.py @@ -203,8 +203,11 @@ async def log_action( masked_body_json = {} if has_json_body: - request_body = await request.json() - if isinstance(request_body, dict): + # ``request_body`` stays empty unless the body parses to a dict, so the ``.items()`` + # calls below never see a list or a bare string. The endpoint rejects those with 422. + parsed_body = await request.json() + if isinstance(parsed_body, dict): + request_body = parsed_body masked_body_json = {k: secrets_masker.redact(v, k) for k, v in request_body.items()} if event_name in skip_dry_run_events and request_body.get("dry_run", True): diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py index a9822e5afaff9..84563368126aa 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_connections.py @@ -368,6 +368,28 @@ def test_post_should_respond_422_for_invalid_conn_id(self, test_client, body): ] } + @pytest.mark.parametrize( + "body", + [ + [{"connection_id": TEST_CONN_ID, "conn_type": TEST_CONN_TYPE}], + '{"connection_id": "a"}', + 42, + ], + ids=["list", "string", "number"], + ) + def test_post_should_respond_422_for_non_dict_json_body(self, test_client, session, body): + """The audit-log dependency reads the body before validation, so a non-object body still gets a 422.""" + response = test_client.post("/connections", json=body) + assert response.status_code == 422 + assert response.json()["detail"][0]["loc"] == ["body"] + _check_last_log( + session, + dag_id=None, + event="post_connection", + logical_date=None, + expected_extra={"method": "POST"}, + ) + @conf_vars({("core", "multi_team"): "False"}) def test_post_rejects_team_name_when_multi_team_disabled(self, test_client): response = test_client.post( diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py index ec5f915dc4381..9a61130e3c15d 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_variables.py @@ -805,6 +805,24 @@ def test_post_should_respond_422_when_key_too_large(self, test_client): ] } + @pytest.mark.parametrize( + "body", + [[{"key": "new variable key", "value": "new variable value"}], '{"key": "a"}', 42], + ids=["list", "string", "number"], + ) + def test_post_should_respond_422_for_non_dict_json_body(self, test_client, session, body): + """The audit-log dependency reads the body before validation, so a non-object body still gets a 422.""" + response = test_client.post("/variables", json=body) + assert response.status_code == 422 + assert response.json()["detail"][0]["loc"] == ["body"] + check_last_log( + session, + dag_id=None, + event="post_variable", + logical_date=None, + expected_extra={"method": "POST"}, + ) + @conf_vars({("core", "multi_team"): "False"}) def test_post_rejects_team_name_when_multi_team_disabled(self, test_client): body = { From 60e0f608bc129075bf957db9adb19b045b94f737 Mon Sep 17 00:00:00 2001 From: PoAn Yang Date: Thu, 10 Sep 2026 10:15:49 +0900 Subject: [PATCH 2/2] Simplify the comment Signed-off-by: PoAn Yang --- airflow-core/src/airflow/api_fastapi/logging/decorators.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/logging/decorators.py b/airflow-core/src/airflow/api_fastapi/logging/decorators.py index 64587c432a02c..b459d784b13b3 100644 --- a/airflow-core/src/airflow/api_fastapi/logging/decorators.py +++ b/airflow-core/src/airflow/api_fastapi/logging/decorators.py @@ -203,8 +203,7 @@ async def log_action( masked_body_json = {} if has_json_body: - # ``request_body`` stays empty unless the body parses to a dict, so the ``.items()`` - # calls below never see a list or a bare string. The endpoint rejects those with 422. + # Non-dict bodies fall through to the endpoint's own 422. parsed_body = await request.json() if isinstance(parsed_body, dict): request_body = parsed_body