From 390d1fa3ae287d82b6b1da26b66ea89cb6165622 Mon Sep 17 00:00:00 2001 From: Victor Sima Date: Sun, 20 Sep 2026 11:43:22 -0400 Subject: [PATCH 1/2] Document the 409 on favorite_dag and return a readable conflict message favorite_dag returns 409 when the Dag is already a favorite, and its test has asserted that since #51264, but the response was never declared, so it is missing from the OpenAPI spec and every client generated from it. The 409 also came from the generic unique-constraint handler, so a repeat favorite answered "Serious error when handling your request" and logged an error, while unfavorite_dag answers "Dag is not marked as favorite". Check for the row first, as unfavorite_dag does, and declare the 409. --- .../core_api/openapi/v2-rest-api-generated.yaml | 6 ++++++ .../api_fastapi/core_api/routes/public/dags.py | 15 ++++++++++++++- .../ui/openapi-gen/requests/services.gen.ts | 1 + .../airflow/ui/openapi-gen/requests/types.gen.ts | 4 ++++ .../core_api/routes/public/test_dags.py | 14 ++++++++++++++ 5 files changed, 39 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index a9e9be01f4a38..fecc061c91c79 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -4474,6 +4474,12 @@ paths: schema: $ref: '#/components/schemas/HTTPExceptionResponse' description: Not Found + '409': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPExceptionResponse' + description: Conflict '422': description: Validation Error content: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py index 62c7eec91ac4f..3b1f275a8b5da 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py @@ -430,7 +430,7 @@ def patch_dags( @dags_router.post( "/{dag_id}/favorite", status_code=status.HTTP_204_NO_CONTENT, - responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]), + responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND, status.HTTP_409_CONFLICT]), dependencies=[Depends(requires_access_dag(method="GET")), Depends(action_logging())], ) def favorite_dag(dag_id: str, session: SessionDep, user: GetUserDep): @@ -440,6 +440,19 @@ def favorite_dag(dag_id: str, session: SessionDep, user: GetUserDep): raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Dag with id '{dag_id}' not found") user_id = str(user.get_id()) + + favorite_exists = session.execute( + select(DagFavorite) + .where( + DagFavorite.dag_id == dag_id, + DagFavorite.user_id == user_id, + ) + .limit(1) + ).first() + + if favorite_exists: + raise HTTPException(status.HTTP_409_CONFLICT, detail="Dag is already marked as favorite") + session.execute(insert(DagFavorite).values(dag_id=dag_id, user_id=user_id)) 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 baf23138c35a8..600c15d40349b 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 @@ -1998,6 +1998,7 @@ export class DagService { 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', + 409: 'Conflict', 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 91ddaf68f7d26..8329aed1ee4e2 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 @@ -6638,6 +6638,10 @@ export type $OpenApiTs = { * Not Found */ 404: HTTPExceptionResponse; + /** + * Conflict + */ + 409: HTTPExceptionResponse; /** * Validation Error */ diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py index fa2a2a7099255..7ea6233ab31f5 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py @@ -1223,6 +1223,19 @@ def test_favoriting_already_favorited_dag_returns_409(self, test_client): response = test_client.post(f"/dags/{DAG1_ID}/favorite") assert response.status_code == 409 + assert response.json()["detail"] == "Dag is already marked as favorite" + + def test_favorite_dag_existence_check_is_bounded(self, test_client): + """The existing-favorite existence probe must ask the DB for one row.""" + with capture_orm_selects("dag_favorite") as statements: + response = test_client.post(f"/dags/{DAG1_ID}/favorite") + + assert response.status_code == 204 + assert statements, "expected the endpoint to query the dag_favorite table" + for sql in statements: + assert re.search(r"\bLIMIT 1\b", sql), ( + f"favorite existence check is not bounded to one row: {sql}" + ) class TestUnfavoriteDag(TestDagEndpoint): @@ -1263,6 +1276,7 @@ def test_unfavorite_dag_should_response_403(self, unauthorized_test_client): def test_unfavoriting_dag_that_is_not_favorite_returns_409(self, test_client): response = test_client.post(f"/dags/{DAG1_ID}/unfavorite") assert response.status_code == 409 + assert response.json()["detail"] == "Dag is not marked as favorite" def test_unfavorite_dag_existence_check_is_bounded(self, test_client, session): """The existing-favorite existence probe must ask the DB for one row.""" From 5e5201f6b0dc9a25ea7f9e1a2351f48a67e64b2e Mon Sep 17 00:00:00 2001 From: Victor Sima Date: Sun, 20 Sep 2026 11:53:34 -0400 Subject: [PATCH 2/2] Drop the unfavorite body assertion It pinned behaviour this change does not touch, which the review guidelines count as padding rather than coverage. --- .../tests/unit/api_fastapi/core_api/routes/public/test_dags.py | 1 - 1 file changed, 1 deletion(-) diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py index 7ea6233ab31f5..73d2d08913cb7 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py @@ -1276,7 +1276,6 @@ def test_unfavorite_dag_should_response_403(self, unauthorized_test_client): def test_unfavoriting_dag_that_is_not_favorite_returns_409(self, test_client): response = test_client.post(f"/dags/{DAG1_ID}/unfavorite") assert response.status_code == 409 - assert response.json()["detail"] == "Dag is not marked as favorite" def test_unfavorite_dag_existence_check_is_bounded(self, test_client, session): """The existing-favorite existence probe must ask the DB for one row."""