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 f4cc43d37cd Handle undecryptable Variable values gracefully in Stable 
REST API (#65452)
f4cc43d37cd is described below

commit f4cc43d37cdc74b3cf08b5d5c8eb5e0a5e90e6ec
Author: Md Zeeshan alam <[email protected]>
AuthorDate: Mon Jun 1 14:34:07 2026 +0530

    Handle undecryptable Variable values gracefully in Stable REST API (#65452)
    
    * 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
    
    ---------
    
    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 f94369b5980..75fefdd656b 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
@@ -33,7 +33,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 8c1097adbb1..5869ad434a5 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
@@ -16188,7 +16188,9 @@ components:
           type: string
           title: Key
         value:
-          type: string
+          anyOf:
+          - type: string
+          - type: 'null'
           title: Value
         description:
           anyOf:
@@ -16206,7 +16208,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 742a18b09ef..28e04804ca9 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
@@ -7425,7 +7425,14 @@ export const $VariableResponse = {
             title: 'Key'
         },
         value: {
-            type: 'string',
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
             title: 'Value'
         },
         description: {
@@ -7456,7 +7463,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 16929844a94..25a5fc6e76d 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
@@ -1840,7 +1840,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 3ab1888c332..f3dcbdb8cc0 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 93a96d54cad..c77816a096f 100644
--- a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
@@ -92,9 +92,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 cd3ecbb33ee..51ec35ac493 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
@@ -276,6 +276,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 4ad4fd39cb9..c07e6e999c1 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -1081,7 +1081,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