This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 f7e827b86e5 Return 404 from task state store endpoints for unknown
task instances (#70983)
f7e827b86e5 is described below
commit f7e827b86e51951304b6cdeccc1de7a04d14cce2
Author: Jyun-An Chen <[email protected]>
AuthorDate: Wed Aug 12 04:26:14 2026 +0800
Return 404 from task state store endpoints for unknown task instances
(#70983)
---
.../core_api/openapi/v2-rest-api-generated.yaml | 68 +++++-----
.../core_api/routes/public/task_state_store.py | 149 ++++++++++++---------
.../ui/openapi-gen/queries/ensureQueryData.ts | 2 +-
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 2 +-
.../src/airflow/ui/openapi-gen/queries/queries.ts | 10 +-
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 2 +-
.../ui/openapi-gen/requests/services.gen.ts | 31 +++--
.../airflow/ui/openapi-gen/requests/types.gen.ts | 4 -
.../routes/public/test_task_state_store.py | 57 ++++++++
9 files changed, 198 insertions(+), 127 deletions(-)
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 81b0a710c9e..b3ed8a3a2b8 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
@@ -6021,12 +6021,6 @@ paths:
schema:
$ref: '#/components/schemas/HTTPExceptionResponse'
description: Forbidden
- '404':
- content:
- application/json:
- schema:
- $ref: '#/components/schemas/HTTPExceptionResponse'
- description: Not Found
'422':
description: Validation Error
content:
@@ -6067,6 +6061,13 @@ paths:
schema:
type: string
title: Task Id
+ - name: all_map_indices
+ in: query
+ required: false
+ schema:
+ type: boolean
+ default: false
+ title: All Map Indices
- name: map_index
in: query
required: false
@@ -6075,13 +6076,6 @@ paths:
minimum: -1
default: -1
title: Map Index
- - name: all_map_indices
- in: query
- required: false
- schema:
- type: boolean
- default: false
- title: All Map Indices
responses:
'204':
description: Successful Response
@@ -6120,6 +6114,12 @@ paths:
- OAuth2PasswordBearer: []
- HTTPBearer: []
parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Key
- name: dag_id
in: path
required: true
@@ -6138,12 +6138,6 @@ paths:
schema:
type: string
title: Task Id
- - name: key
- in: path
- required: true
- schema:
- type: string
- title: Key
- name: map_index
in: query
required: false
@@ -6193,6 +6187,12 @@ paths:
- OAuth2PasswordBearer: []
- HTTPBearer: []
parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Key
- name: dag_id
in: path
required: true
@@ -6211,12 +6211,6 @@ paths:
schema:
type: string
title: Task Id
- - name: key
- in: path
- required: true
- schema:
- type: string
- title: Key
- name: map_index
in: query
required: false
@@ -6268,6 +6262,12 @@ paths:
- OAuth2PasswordBearer: []
- HTTPBearer: []
parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Key
- name: dag_id
in: path
required: true
@@ -6286,12 +6286,6 @@ paths:
schema:
type: string
title: Task Id
- - name: key
- in: path
- required: true
- schema:
- type: string
- title: Key
- name: map_index
in: query
required: false
@@ -6347,6 +6341,12 @@ paths:
- OAuth2PasswordBearer: []
- HTTPBearer: []
parameters:
+ - name: key
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Key
- name: dag_id
in: path
required: true
@@ -6365,12 +6365,6 @@ paths:
schema:
type: string
title: Task Id
- - name: key
- in: path
- required: true
- schema:
- type: string
- title: Key
- name: map_index
in: query
required: false
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_state_store.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_state_store.py
index 54e1337215c..0d844b8b3ac 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_state_store.py
+++
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/task_state_store.py
@@ -18,7 +18,7 @@ from __future__ import annotations
import json
from datetime import datetime, timedelta, timezone
-from typing import Annotated, Literal
+from typing import TYPE_CHECKING, Annotated, Literal
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy import select
@@ -41,16 +41,83 @@ from airflow.models.task_state_store import
TaskStateStoreModel
from airflow.models.taskinstance import TaskInstance as TI
from airflow.state.metastore import _get_db_backend
+if TYPE_CHECKING:
+ from sqlalchemy.orm import Session
+
task_state_store_router = AirflowRouter(
tags=["Task State Store"],
prefix="/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store",
)
-def _get_scope(dag_id: str, dag_run_id: str, task_id: str, map_index: int) ->
TaskScope:
+def _require_task_instance(
+ dag_id: str,
+ dag_run_id: str,
+ task_id: str,
+ map_index: int | None,
+ session: Session,
+) -> None:
+ """Raise 404 unless the task instance exists. ``map_index=None`` matches
any map index."""
+ statement = select(TI.task_id).where(
+ TI.dag_id == dag_id,
+ TI.run_id == dag_run_id,
+ TI.task_id == task_id,
+ )
+ if map_index is not None:
+ statement = statement.where(TI.map_index == map_index)
+ if session.scalar(statement.limit(1)) is None:
+ addressed_by = "all_map_indices=True" if map_index is None else
f"map_index={map_index}"
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=(
+ f"Task instance not found for dag_id={dag_id!r},
run_id={dag_run_id!r}, "
+ f"task_id={task_id!r}, {addressed_by}"
+ ),
+ )
+
+
+def _resolve_scope(
+ dag_id: str,
+ dag_run_id: str,
+ task_id: str,
+ map_index: Annotated[int, Query(ge=-1)] = -1,
+) -> TaskScope:
+ """Map the path and query parameters onto the task instance they
address."""
return TaskScope(dag_id=dag_id, run_id=dag_run_id, task_id=task_id,
map_index=map_index)
+TaskScopeDep = Annotated[TaskScope, Depends(_resolve_scope)]
+
+
+def _validate_scope(scope: TaskScopeDep, session: SessionDep) -> TaskScope:
+ """Resolve the scope, 404ing when the task instance it addresses does not
exist."""
+ _require_task_instance(scope.dag_id, scope.run_id, scope.task_id,
scope.map_index, session)
+ return scope
+
+
+ValidatedTaskScopeDep = Annotated[TaskScope, Depends(_validate_scope)]
+
+
+def _validate_clear_scope(
+ scope: TaskScopeDep,
+ session: SessionDep,
+ all_map_indices: Annotated[bool, Query()] = False,
+) -> TaskScope:
+ """
+ Resolve the scope for a clear request, 404ing when the task instance does
not exist.
+
+ ``all_map_indices`` addresses the task across every index, so it is
validated against any
+ instance -- an expanded mapped task has no ``map_index=-1`` instance to
check.
+ """
+ _require_task_instance(
+ scope.dag_id, scope.run_id, scope.task_id, None if all_map_indices
else scope.map_index, session
+ )
+ return scope
+
+
+ValidatedClearTaskScopeDep = Annotated[TaskScope,
Depends(_validate_clear_scope)]
+
+
def _resolve_expires_at(expires_at: datetime | None | Literal["default"]) ->
datetime | None:
"""
Resolve the expires_at value from the request body.
@@ -72,35 +139,15 @@ def _resolve_expires_at(expires_at: datetime | None |
Literal["default"]) -> dat
return expires_at
-def _require_ti(dag_id: str, dag_run_id: str, task_id: str, map_index: int,
session: SessionDep) -> None:
- ti_exists = session.scalar(
- select(TI.task_id).where(
- TI.dag_id == dag_id,
- TI.run_id == dag_run_id,
- TI.task_id == task_id,
- TI.map_index == map_index,
- )
- )
- if ti_exists is None:
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Task instance not found for dag_id={dag_id!r},
run_id={dag_run_id!r}, task_id={task_id!r}, map_index={map_index}",
- )
-
-
@task_state_store_router.get(
"",
- responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]),
dependencies=[Depends(requires_access_dag(method="GET",
access_entity=DagAccessEntity.TASK_INSTANCE))],
)
def list_task_state_store(
- dag_id: str,
- dag_run_id: str,
- task_id: str,
+ scope: TaskScopeDep,
limit: QueryLimit,
offset: QueryOffset,
session: SessionDep,
- map_index: Annotated[int, Query(ge=-1)] = -1,
) -> TaskStateStoreCollectionResponse:
"""List all task state store entries for a task instance."""
base = (
@@ -111,10 +158,10 @@ def list_task_state_store(
TaskStateStoreModel.expires_at,
)
.where(
- TaskStateStoreModel.dag_id == dag_id,
- TaskStateStoreModel.run_id == dag_run_id,
- TaskStateStoreModel.task_id == task_id,
- TaskStateStoreModel.map_index == map_index,
+ TaskStateStoreModel.dag_id == scope.dag_id,
+ TaskStateStoreModel.run_id == scope.run_id,
+ TaskStateStoreModel.task_id == scope.task_id,
+ TaskStateStoreModel.map_index == scope.map_index,
)
.order_by(TaskStateStoreModel.key.asc())
)
@@ -142,12 +189,9 @@ def list_task_state_store(
dependencies=[Depends(requires_access_dag(method="GET",
access_entity=DagAccessEntity.TASK_INSTANCE))],
)
def get_task_state_store(
- dag_id: str,
- dag_run_id: str,
- task_id: str,
+ scope: TaskScopeDep,
key: str,
session: SessionDep,
- map_index: Annotated[int, Query(ge=-1)] = -1,
) -> TaskStateStoreResponse:
"""Get a single task state store entry."""
row = session.execute(
@@ -157,10 +201,10 @@ def get_task_state_store(
TaskStateStoreModel.updated_at,
TaskStateStoreModel.expires_at,
).where(
- TaskStateStoreModel.dag_id == dag_id,
- TaskStateStoreModel.run_id == dag_run_id,
- TaskStateStoreModel.task_id == task_id,
- TaskStateStoreModel.map_index == map_index,
+ TaskStateStoreModel.dag_id == scope.dag_id,
+ TaskStateStoreModel.run_id == scope.run_id,
+ TaskStateStoreModel.task_id == scope.task_id,
+ TaskStateStoreModel.map_index == scope.map_index,
TaskStateStoreModel.key == key,
)
).one_or_none()
@@ -181,18 +225,13 @@ def get_task_state_store(
dependencies=[Depends(requires_access_dag(method="PUT",
access_entity=DagAccessEntity.TASK_INSTANCE))],
)
def set_task_state_store(
- dag_id: str,
- dag_run_id: str,
- task_id: str,
+ scope: ValidatedTaskScopeDep,
key: str,
body: TaskStateStoreBody,
session: SessionDep,
- map_index: Annotated[int, Query(ge=-1)] = -1,
) -> None:
"""Set a task state store value. Creates or overwrites the key."""
- _require_ti(dag_id, dag_run_id, task_id, map_index, session)
expires_at = _resolve_expires_at(body.expires_at)
- scope = _get_scope(dag_id, dag_run_id, task_id, map_index)
try:
_get_db_backend().set(scope, key, json.dumps(body.value),
expires_at=expires_at, session=session)
except ValueError as e:
@@ -206,23 +245,18 @@ def set_task_state_store(
dependencies=[Depends(requires_access_dag(method="PUT",
access_entity=DagAccessEntity.TASK_INSTANCE))],
)
def patch_task_state_store(
- dag_id: str,
- dag_run_id: str,
- task_id: str,
+ scope: ValidatedTaskScopeDep,
key: str,
body: TaskStateStorePatchBody,
session: SessionDep,
- map_index: Annotated[int, Query(ge=-1)] = -1,
) -> None:
"""Update the value of an existing task state store key."""
- _require_ti(dag_id, dag_run_id, task_id, map_index, session)
-
existing = session.execute(
select(TaskStateStoreModel.expires_at).where(
- TaskStateStoreModel.dag_id == dag_id,
- TaskStateStoreModel.run_id == dag_run_id,
- TaskStateStoreModel.task_id == task_id,
- TaskStateStoreModel.map_index == map_index,
+ TaskStateStoreModel.dag_id == scope.dag_id,
+ TaskStateStoreModel.run_id == scope.run_id,
+ TaskStateStoreModel.task_id == scope.task_id,
+ TaskStateStoreModel.map_index == scope.map_index,
TaskStateStoreModel.key == key,
)
).one_or_none()
@@ -233,7 +267,6 @@ def patch_task_state_store(
detail=f"Task state store key {key!r} not found",
)
- scope = _get_scope(dag_id, dag_run_id, task_id, map_index)
_get_db_backend().set(scope, key, json.dumps(body.value),
expires_at=existing.expires_at, session=session)
@@ -244,15 +277,11 @@ def patch_task_state_store(
dependencies=[Depends(requires_access_dag(method="DELETE",
access_entity=DagAccessEntity.TASK_INSTANCE))],
)
def delete_task_state_store(
- dag_id: str,
- dag_run_id: str,
- task_id: str,
+ scope: ValidatedTaskScopeDep,
key: str,
session: SessionDep,
- map_index: Annotated[int, Query(ge=-1)] = -1,
) -> None:
"""Delete a single task state store key. No-op if the key does not
exist."""
- scope = _get_scope(dag_id, dag_run_id, task_id, map_index)
_get_db_backend().delete(scope, key, session=session)
@@ -263,11 +292,8 @@ def delete_task_state_store(
dependencies=[Depends(requires_access_dag(method="DELETE",
access_entity=DagAccessEntity.TASK_INSTANCE))],
)
def clear_task_state_store(
- dag_id: str,
- dag_run_id: str,
- task_id: str,
+ scope: ValidatedClearTaskScopeDep,
session: SessionDep,
- map_index: Annotated[int, Query(ge=-1)] = -1,
all_map_indices: Annotated[bool, Query()] = False,
) -> None:
"""
@@ -276,5 +302,4 @@ def clear_task_state_store(
When ``all_map_indices=true``, state store is cleared for every map index
of the task and
the ``map_index`` parameter is ignored.
"""
- scope = _get_scope(dag_id, dag_run_id, task_id, map_index)
_get_db_backend().clear(scope, all_map_indices=all_map_indices,
session=session)
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 637e37a62f4..c7d379c85d9 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1583,10 +1583,10 @@ export const
ensureUseTaskStateStoreServiceListTaskStateStoreData = (queryClient
* Get Task State Store
* Get a single task state store entry.
* @param data The data for the request.
+* @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.key
* @param data.mapIndex
* @returns TaskStateStoreResponse Successful Response
* @throws ApiError
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index d7d3fecb52c..f0af02f94f6 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1583,10 +1583,10 @@ export const
prefetchUseTaskStateStoreServiceListTaskStateStore = (queryClient:
* Get Task State Store
* Get a single task state store entry.
* @param data The data for the request.
+* @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.key
* @param data.mapIndex
* @returns TaskStateStoreResponse Successful Response
* @throws ApiError
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 1b993afa9b0..0c69edfd97b 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1583,10 +1583,10 @@ export const useTaskStateStoreServiceListTaskStateStore
= <TData = Common.TaskSt
* Get Task State Store
* Get a single task state store entry.
* @param data The data for the request.
+* @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.key
* @param data.mapIndex
* @returns TaskStateStoreResponse Successful Response
* @throws ApiError
@@ -2509,10 +2509,10 @@ export const
useAssetStateStoreServiceSetAssetStateStore = <TData = Common.Asset
* Set Task State Store
* Set a task state store value. Creates or overwrites the key.
* @param data The data for the request.
+* @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.key
* @param data.requestBody
* @param data.mapIndex
* @returns void Successful Response
@@ -2921,10 +2921,10 @@ export const usePoolServiceBulkPools = <TData =
Common.PoolServiceBulkPoolsMutat
* Patch Task State Store
* Update the value of an existing task state store key.
* @param data The data for the request.
+* @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.key
* @param data.requestBody
* @param data.mapIndex
* @returns unknown Successful Response
@@ -3168,8 +3168,8 @@ export const
useAssetStateStoreServiceDeleteAssetStateStore = <TData = Common.As
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.mapIndex
* @param data.allMapIndices
+* @param data.mapIndex
* @returns void Successful Response
* @throws ApiError
*/
@@ -3190,10 +3190,10 @@ export const
useTaskStateStoreServiceClearTaskStateStore = <TData = Common.TaskS
* Delete Task State Store
* Delete a single task state store key. No-op if the key does not exist.
* @param data The data for the request.
+* @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.key
* @param data.mapIndex
* @returns void Successful Response
* @throws ApiError
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 4d2dc42adca..ec1e3504f7d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1583,10 +1583,10 @@ export const
useTaskStateStoreServiceListTaskStateStoreSuspense = <TData = Commo
* Get Task State Store
* Get a single task state store entry.
* @param data The data for the request.
+* @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
-* @param data.key
* @param data.mapIndex
* @returns TaskStateStoreResponse Successful Response
* @throws ApiError
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 09e450335b1..0fc5c1c220f 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
@@ -3891,7 +3891,6 @@ export class TaskStateStoreService {
errors: {
401: 'Unauthorized',
403: 'Forbidden',
- 404: 'Not Found',
422: 'Validation Error'
}
});
@@ -3907,8 +3906,8 @@ export class TaskStateStoreService {
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
- * @param data.mapIndex
* @param data.allMapIndices
+ * @param data.mapIndex
* @returns void Successful Response
* @throws ApiError
*/
@@ -3922,8 +3921,8 @@ export class TaskStateStoreService {
task_id: data.taskId
},
query: {
- map_index: data.mapIndex,
- all_map_indices: data.allMapIndices
+ all_map_indices: data.allMapIndices,
+ map_index: data.mapIndex
},
errors: {
401: 'Unauthorized',
@@ -3938,10 +3937,10 @@ export class TaskStateStoreService {
* Get Task State Store
* Get a single task state store entry.
* @param data The data for the request.
+ * @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
- * @param data.key
* @param data.mapIndex
* @returns TaskStateStoreResponse Successful Response
* @throws ApiError
@@ -3951,10 +3950,10 @@ export class TaskStateStoreService {
method: 'GET',
url:
'/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key}',
path: {
+ key: data.key,
dag_id: data.dagId,
dag_run_id: data.dagRunId,
- task_id: data.taskId,
- key: data.key
+ task_id: data.taskId
},
query: {
map_index: data.mapIndex
@@ -3972,10 +3971,10 @@ export class TaskStateStoreService {
* Set Task State Store
* Set a task state store value. Creates or overwrites the key.
* @param data The data for the request.
+ * @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
- * @param data.key
* @param data.requestBody
* @param data.mapIndex
* @returns void Successful Response
@@ -3986,10 +3985,10 @@ export class TaskStateStoreService {
method: 'PUT',
url:
'/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key}',
path: {
+ key: data.key,
dag_id: data.dagId,
dag_run_id: data.dagRunId,
- task_id: data.taskId,
- key: data.key
+ task_id: data.taskId
},
query: {
map_index: data.mapIndex
@@ -4009,10 +4008,10 @@ export class TaskStateStoreService {
* Patch Task State Store
* Update the value of an existing task state store key.
* @param data The data for the request.
+ * @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
- * @param data.key
* @param data.requestBody
* @param data.mapIndex
* @returns unknown Successful Response
@@ -4023,10 +4022,10 @@ export class TaskStateStoreService {
method: 'PATCH',
url:
'/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key}',
path: {
+ key: data.key,
dag_id: data.dagId,
dag_run_id: data.dagRunId,
- task_id: data.taskId,
- key: data.key
+ task_id: data.taskId
},
query: {
map_index: data.mapIndex
@@ -4046,10 +4045,10 @@ export class TaskStateStoreService {
* Delete Task State Store
* Delete a single task state store key. No-op if the key does not exist.
* @param data The data for the request.
+ * @param data.key
* @param data.dagId
* @param data.dagRunId
* @param data.taskId
- * @param data.key
* @param data.mapIndex
* @returns void Successful Response
* @throws ApiError
@@ -4059,10 +4058,10 @@ export class TaskStateStoreService {
method: 'DELETE',
url:
'/api/v2/dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/state-store/{key}',
path: {
+ key: data.key,
dag_id: data.dagId,
dag_run_id: data.dagRunId,
- task_id: data.taskId,
- key: data.key
+ task_id: data.taskId
},
query: {
map_index: data.mapIndex
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 13252e1126f..92b30f77199 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
@@ -7803,10 +7803,6 @@ export type $OpenApiTs = {
* Forbidden
*/
403: HTTPExceptionResponse;
- /**
- * Not Found
- */
- 404: HTTPExceptionResponse;
/**
* Validation Error
*/
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py
index 650858656da..ac117b31d04 100644
---
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py
+++
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_task_state_store.py
@@ -498,3 +498,60 @@ class
TestRoutesNeverCallCustomBackend(TestTaskStateEndpoint):
getattr(test_client, method)(path, **kwargs)
mock_get_backend.assert_not_called()
+
+
+class TestUnknownTaskInstance(TestTaskStateEndpoint):
+ UNKNOWN_TASK_URL =
f"/dags/{DAG_ID}/dagRuns/{RUN_ID}/taskInstances/no_such_task/state-store"
+ UNKNOWN_RUN_URL =
f"/dags/{DAG_ID}/dagRuns/no_such_run/taskInstances/{TASK_ID}/state-store"
+
+ @pytest.mark.parametrize(
+ ("method", "suffix", "kwargs"),
+ [
+ ("put", "/job_id", {"json": {"value": "v"}}),
+ ("patch", "/job_id", {"json": {"value": "v"}}),
+ ("delete", "/job_id", {}),
+ ("delete", "", {}),
+ ("delete", "?all_map_indices=true", {}),
+ ],
+ )
+ @pytest.mark.parametrize("base", [UNKNOWN_TASK_URL, UNKNOWN_RUN_URL],
ids=["task_id", "run_id"])
+ def test_returns_404(self, test_client, base, method, suffix, kwargs):
+ response = getattr(test_client, method)(f"{base}{suffix}", **kwargs)
+ assert response.status_code == 404
+ assert "Task instance not found" in response.json()["detail"]
+
+ @pytest.mark.parametrize(
+ ("method", "suffix", "kwargs"),
+ [
+ ("put", "/job_id", {"json": {"value": "v"}}),
+ ("patch", "/job_id", {"json": {"value": "v"}}),
+ ("delete", "/job_id", {}),
+ ("delete", "", {}),
+ ],
+ )
+ def test_unknown_map_index_returns_404(self, test_client, method, suffix,
kwargs):
+ response = getattr(test_client,
method)(f"{BASE_URL}{suffix}?map_index=7", **kwargs)
+ assert response.status_code == 404
+ assert "map_index=7" in response.json()["detail"]
+
+ def test_clear_all_map_indices_reports_the_indices_it_looked_for(self,
test_client):
+ response =
test_client.delete(f"{self.UNKNOWN_TASK_URL}?all_map_indices=true")
+ assert response.status_code == 404
+ assert response.json()["detail"].endswith("task_id='no_such_task',
all_map_indices=True")
+
+ @pytest.mark.parametrize("base", [UNKNOWN_TASK_URL, UNKNOWN_RUN_URL],
ids=["task_id", "run_id"])
+ def test_list_returns_an_empty_collection(self, test_client, base):
+ response = test_client.get(base)
+ assert response.status_code == 200
+ assert response.json() == {"task_state_store": [], "total_entries": 0}
+
+ @pytest.mark.parametrize("base", [UNKNOWN_TASK_URL, UNKNOWN_RUN_URL],
ids=["task_id", "run_id"])
+ def test_get_reports_the_missing_key(self, test_client, base):
+ response = test_client.get(f"{base}/job_id")
+ assert response.status_code == 404
+ assert response.json()["detail"] == "Task state store key 'job_id' not
found"
+
+ def test_missing_key_still_reports_the_key_not_the_task_instance(self,
test_client):
+ response = test_client.get(f"{BASE_URL}/no_such_key")
+ assert response.status_code == 404
+ assert response.json()["detail"] == "Task state store key
'no_such_key' not found"