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

Reply via email to