This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new eccbdb1b082 UI: Return 400 instead of 500 from structure_data on 
malformed asset_expression (#67489)
eccbdb1b082 is described below

commit eccbdb1b082be73b870e0a354d980ce2f542cc46
Author: Deepak kumar <[email protected]>
AuthorDate: Mon Jun 1 07:43:02 2026 -0700

    UI: Return 400 instead of 500 from structure_data on malformed 
asset_expression (#67489)
    
    * UI: Return clear 500 detail from structure_data when asset_expression is 
malformed
    
    The /structure/structure_data endpoint calls get_upstream_assets() to walk 
the
    serialized Dag's asset_expression. If the stored expression contains an 
unknown
    key or asset type, get_upstream_assets() raises TypeError("Unsupported 
type: ...").
    The exception escaped uncaught and FastAPI returned a generic
    {"detail": "Internal Server Error"} body with no context about which Dag
    triggered it, forcing operators to dig through server logs to identify the
    broken Dag.
    
    Wrap the call in try/except TypeError and re-raise as HTTPException(500) 
with a
    detail message identifying the Dag id and version. Still a 500 (the 
underlying
    data corruption is genuinely server-side, not bad client input), but now 
with a
    controlled, debuggable response body.
    
    Regression test mocks get_upstream_assets to raise TypeError and asserts the
    response is 500 with a detail message that includes the Dag id.
    
    * Use 400 BAD_REQUEST for malformed asset_expression per review feedback
    
    Per @jason810496 review feedback on #67489: the malformed asset_expression
    ultimately originates from user-authored Dag code (via the Task SDK), so the
    appropriate response is 400 BAD_REQUEST rather than 500 
INTERNAL_SERVER_ERROR.
    
    - Change status code from 500 to 400 in structure_data.
    - Add HTTP_400_BAD_REQUEST to create_openapi_http_exception_doc so the 
OpenAPI
      spec advertises the new error response.
    - Update regression test to assert 400 and rename accordingly.
    
    Detail message is unchanged per reviewer: "It's fine to add more context".
    
    * Revert uv.lock diff
    
    ---------
    
    Co-authored-by: pierrejeambrun <[email protected]>
---
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  6 ++++++
 .../api_fastapi/core_api/routes/ui/structure.py    | 19 ++++++++++++----
 .../ui/openapi-gen/requests/services.gen.ts        |  1 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  4 ++++
 .../core_api/routes/ui/test_structure.py           | 25 ++++++++++++++++++++++
 5 files changed, 51 insertions(+), 4 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index e6fb850247d..52a09dcbb7b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -991,6 +991,12 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/StructureDataResponse'
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Bad Request
         '404':
           content:
             application/json:
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py
index 87184788413..597f44db424 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/structure.py
@@ -42,7 +42,12 @@ structure_router = AirflowRouter(tags=["Structure"], 
prefix="/structure")
 
 @structure_router.get(
     "/structure_data",
-    responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
+    responses=create_openapi_http_exception_doc(
+        [
+            status.HTTP_400_BAD_REQUEST,
+            status.HTTP_404_NOT_FOUND,
+        ]
+    ),
     dependencies=[
         Depends(requires_access_dag("GET")),
         Depends(requires_access_dag("GET", DagAccessEntity.DEPENDENCIES)),
@@ -161,9 +166,15 @@ def structure_data(
                 )
 
         if (asset_expression := serialized_dag.dag_model.asset_expression) and 
entry_node_ref:
-            upstream_asset_nodes, upstream_asset_edges = get_upstream_assets(
-                asset_expression, entry_node_ref["id"]
-            )
+            try:
+                upstream_asset_nodes, upstream_asset_edges = 
get_upstream_assets(
+                    asset_expression, entry_node_ref["id"]
+                )
+            except TypeError as e:
+                raise HTTPException(
+                    status.HTTP_400_BAD_REQUEST,
+                    f"Malformed asset_expression in Dag {dag_id!r} version 
{version_number}: {e}",
+                ) from e
             data["nodes"] += upstream_asset_nodes
             data["edges"] += upstream_asset_edges
 
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 3e659835303..74502a154c8 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -4783,6 +4783,7 @@ export class StructureService {
                 version_number: data.versionNumber
             },
             errors: {
+                400: 'Bad Request',
                 404: 'Not Found',
                 422: 'Validation Error'
             }
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 77f62f0bded..2000ec81e8f 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -8002,6 +8002,10 @@ export type $OpenApiTs = {
                  * Successful Response
                  */
                 200: StructureDataResponse;
+                /**
+                 * Bad Request
+                 */
+                400: HTTPExceptionResponse;
                 /**
                  * Not Found
                  */
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py
index db436ca3cf9..8e504c132c4 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_structure.py
@@ -709,6 +709,31 @@ class TestStructureDataEndpoint:
         assert response.status_code == 404
         assert response.json()["detail"] == "Dag with id not_existing was not 
found"
 
+    @pytest.mark.usefixtures("make_dags")
+    def test_should_return_400_on_malformed_asset_expression(self, 
test_client):
+        """A TypeError from get_upstream_assets surfaces as a 400 with a clear 
message.
+
+        The asset_expression ultimately comes from user-authored Dag code (via 
the Task SDK),
+        so a malformed expression is bad input that ended up persisted -- not 
a server fault.
+        Without the try/except wrap, the TypeError propagates uncaught and 
FastAPI returns a
+        generic ``{"detail": "Internal Server Error"}`` 500 body with no 
context about which
+        Dag triggered it. With the wrap, the response identifies the Dag and 
version, which
+        is what a caller needs to fix the upstream Dag definition.
+        """
+        with mock.patch(
+            
"airflow.api_fastapi.core_api.routes.ui.structure.get_upstream_assets",
+            side_effect=TypeError("Unsupported type: dict_keys(['weird-op'])"),
+        ):
+            response = test_client.get(
+                "/structure/structure_data",
+                params={"dag_id": DAG_ID, "external_dependencies": True},
+            )
+        assert response.status_code == 400
+        detail = response.json()["detail"]
+        assert "Malformed asset_expression" in detail
+        assert DAG_ID in detail
+        assert "Unsupported type" in detail
+
     def test_should_return_404_when_dag_version_not_found(self, test_client):
         response = test_client.get(
             "/structure/structure_data", params={"dag_id": DAG_ID, 
"version_number": 999}

Reply via email to