This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-2-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-2-test by this push:
new 84fde7e64b1 [v3-2-test] Handle undecryptable Variable values
gracefully in Stable REST API (#65452) (#67828)
84fde7e64b1 is described below
commit 84fde7e64b106414c0925f427f521ea729c03b80
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Jun 1 12:52:26 2026 +0200
[v3-2-test] Handle undecryptable Variable values gracefully in Stable REST
API (#65452) (#67828)
* Allow null values for Variable value field to handle decryption failures
gracefully
* Document rationale in
code, add a regression test, and add newsfragment 65452.bugfix.rst
* Fix CI for #65452: single-line newsfragment, real fernet decrypt mock,
regenerate openapi spec + UI/airflowctl datamodels
* Fix CI for #65452: handle nullable Variable.value in UI, regenerate
openapi/airflowctl datamodels, single-line newsfragment, real fernet decrypt
mock
* Removed all the comments, suggested by the reviewers
---------
(cherry picked from commit f4cc43d37cdc74b3cf08b5d5c8eb5e0a5e90e6ec)
Co-authored-by: Md Zeeshan alam
<[email protected]>
Co-authored-by: Md Zeeshan Alam <[email protected]>
---
.../api_fastapi/core_api/datamodels/variables.py | 2 +-
.../core_api/openapi/v2-rest-api-generated.yaml | 5 +++--
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 11 +++++++--
.../airflow/ui/openapi-gen/requests/types.gen.ts | 2 +-
.../ManageVariable/EditVariableButton.tsx | 2 +-
.../airflow/ui/src/pages/Variables/Variables.tsx | 4 ++--
.../core_api/routes/public/test_variables.py | 26 ++++++++++++++++++++++
.../src/airflowctl/api/datamodels/generated.py | 2 +-
8 files changed, 44 insertions(+), 10 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py
index 9dc7969b69b..a781c79c6a2 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/variables.py
@@ -32,7 +32,7 @@ class VariableResponse(BaseModel):
"""Variable serializer for responses."""
key: str
- val: str = Field(alias="value")
+ val: str | None = Field(alias="value", default=None)
description: str | None
is_encrypted: bool
team_name: str | None
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 ede1d9a9545..92f5dcade8a 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
@@ -14977,7 +14977,9 @@ components:
type: string
title: Key
value:
- type: string
+ anyOf:
+ - type: string
+ - type: 'null'
title: Value
description:
anyOf:
@@ -14995,7 +14997,6 @@ components:
type: object
required:
- key
- - value
- description
- is_encrypted
- team_name
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 0edc8f1c668..1ae88b9433e 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -7041,7 +7041,14 @@ export const $VariableResponse = {
title: 'Key'
},
value: {
- type: 'string',
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
title: 'Value'
},
description: {
@@ -7072,7 +7079,7 @@ export const $VariableResponse = {
}
},
type: 'object',
- required: ['key', 'value', 'description', 'is_encrypted', 'team_name'],
+ required: ['key', 'description', 'is_encrypted', 'team_name'],
title: 'VariableResponse',
description: 'Variable serializer for responses.'
} as const;
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 9d519a7039c..3062baff3a8 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
@@ -1709,7 +1709,7 @@ export type VariableCollectionResponse = {
*/
export type VariableResponse = {
key: string;
- value: string;
+ value?: string | null;
description: string | null;
is_encrypted: boolean;
team_name: string | null;
diff --git
a/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/EditVariableButton.tsx
b/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/EditVariableButton.tsx
index 65d65014c35..264da41bccc 100644
---
a/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/EditVariableButton.tsx
+++
b/airflow-core/src/airflow/ui/src/pages/Variables/ManageVariable/EditVariableButton.tsx
@@ -50,7 +50,7 @@ const EditVariableButton = ({ disabled, variable }: Props) =>
{
description: variable.description ?? "",
key: variable.key,
team_name: variable.team_name ?? "",
- value: formatValue(variable.value),
+ value: formatValue(variable.value ?? ""),
};
const { editVariable, error, isPending, setError } =
useEditVariable(initialVariableValue, {
onSuccessConfirm: onClose,
diff --git a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
index b44a020f5c5..bb7f8bda9c6 100644
--- a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
@@ -94,9 +94,9 @@ const getColumns = ({
cell: ({ row }) => (
<Box minWidth={0} overflowWrap="anywhere" wordBreak="break-word">
<TrimText
- charLimit={open ? row.original.value.length : undefined}
+ charLimit={open ? (row.original.value?.length ?? 0) : undefined}
showTooltip
- text={row.original.value}
+ text={row.original.value ?? null}
/>
</Box>
),
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 7e9f7b9623b..a5baa43c283 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
@@ -254,6 +254,32 @@ class TestGetVariable(TestVariableEndpoint):
body = response.json()
assert f"The Variable with key: `{TEST_VARIABLE_KEY}` was not found"
== body["detail"]
+ def
test_get_should_respond_200_with_null_value_when_decryption_fails(self,
test_client, session):
+ """
+ Regression test for https://github.com/apache/airflow/pull/65452.
+
+ If the stored value cannot be decrypted (for example after a Fernet key
+ rotation) ``Variable.get_val`` returns ``None``. The endpoint must then
+ respond with HTTP 200 and ``"value": null`` instead of failing with an
+ HTTP 500 caused by response-schema validation.
+ """
+ from cryptography.fernet import InvalidToken
+
+ self.create_variables()
+ with mock.patch("airflow.models.variable.get_fernet") as
mock_get_fernet:
+ mock_get_fernet.return_value.decrypt.side_effect = InvalidToken
+ response = test_client.get(f"/variables/{TEST_VARIABLE_KEY}")
+
+ assert response.status_code == 200
+ body = response.json()
+ assert body == {
+ "key": TEST_VARIABLE_KEY,
+ "value": None,
+ "description": TEST_VARIABLE_DESCRIPTION,
+ "is_encrypted": True,
+ "team_name": None,
+ }
+
class TestGetVariables(TestVariableEndpoint):
@pytest.mark.enable_redact
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 3e158bbf739..9b3eff63fb1 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -992,7 +992,7 @@ class VariableResponse(BaseModel):
"""
key: Annotated[str, Field(title="Key")]
- value: Annotated[str, Field(title="Value")]
+ value: Annotated[str | None, Field(title="Value")] = None
description: Annotated[str | None, Field(title="Description")] = None
is_encrypted: Annotated[bool, Field(title="Is Encrypted")]
team_name: Annotated[str | None, Field(title="Team Name")] = None