This is an automated email from the ASF dual-hosted git repository.
eladkal 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 f6621f3015d Fix `TypeError` crashes on `/users/list` and `/roles/list`
in FAB UI caused by concurrent API schema requests (#63986)
f6621f3015d is described below
commit f6621f3015d6686c7c18061d1d3b46b7745be107
Author: Subham <[email protected]>
AuthorDate: Tue Mar 24 15:31:23 2026 +0530
Fix `TypeError` crashes on `/users/list` and `/roles/list` in FAB UI caused
by concurrent API schema requests (#63986)
* Fix TypeError crashes on /users/list and /roles/list in FAB UI caused by
concurrent API schema requests
* Fix TypeError in FAB UI by isolating ProvidersManager discovery and
making MockOptional callable
* Fix unrelated Elasticsearch test failure in FAB UI PR branch
* Revert unrelated Elasticsearch test changes
---
.../core_api/services/ui/connections.py | 95 +++++++++++++++++-----
1 file changed, 76 insertions(+), 19 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
index 96480555c40..12e823ce1d8 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/connections.py
@@ -49,10 +49,11 @@ class HookMetaService:
pass
def __call__(self, form, field):
- pass
+ """No-op call to satisfy WTForms validator protocol."""
+ return None
class MockEnum:
- """Mock for wtforms.validators.Optional."""
+ """Mock for wtforms.validators.AnyOf."""
def __init__(self, allowed_values):
self.allowed_values = allowed_values
@@ -156,23 +157,40 @@ class HookMetaService:
raise ModuleNotFoundError
except ModuleNotFoundError:
sys.modules[mod_name] = MagicMock()
- with (
- mock.patch("wtforms.StringField", HookMetaService.MockStringField),
- mock.patch("wtforms.fields.StringField",
HookMetaService.MockStringField),
- mock.patch("wtforms.fields.simple.StringField",
HookMetaService.MockStringField),
- mock.patch("wtforms.IntegerField",
HookMetaService.MockIntegerField),
- mock.patch("wtforms.fields.IntegerField",
HookMetaService.MockIntegerField),
- mock.patch("wtforms.PasswordField",
HookMetaService.MockPasswordField),
- mock.patch("wtforms.BooleanField",
HookMetaService.MockBooleanField),
- mock.patch("wtforms.fields.BooleanField",
HookMetaService.MockBooleanField),
- mock.patch("wtforms.fields.simple.BooleanField",
HookMetaService.MockBooleanField),
- mock.patch("flask_babel.lazy_gettext", mock_lazy_gettext),
- mock.patch("flask_appbuilder.fieldwidgets.BS3TextFieldWidget",
HookMetaService.MockAnyWidget),
- mock.patch("flask_appbuilder.fieldwidgets.BS3TextAreaFieldWidget",
HookMetaService.MockAnyWidget),
- mock.patch("flask_appbuilder.fieldwidgets.BS3PasswordFieldWidget",
HookMetaService.MockAnyWidget),
- mock.patch("wtforms.validators.Optional",
HookMetaService.MockOptional),
- mock.patch("wtforms.validators.any_of", mock_any_of),
- ):
+
+ # We conditionally inject mock classes for missing dependencies
+ # to ensure `ProvidersManager` can initialize hook connection widgets
+ # without crashing when FAB/WTForms are not installed.
+ if "wtforms.StringField" not in sys.modules:
+ # Only apply mocks if the actual module wasn't loaded beforehand.
+ # This avoids thread-safety issues caused by `unittest.mock.patch`
mutating global states.
+ with (
+ mock.patch("wtforms.StringField",
HookMetaService.MockStringField),
+ mock.patch("wtforms.fields.StringField",
HookMetaService.MockStringField),
+ mock.patch("wtforms.fields.simple.StringField",
HookMetaService.MockStringField),
+ mock.patch("wtforms.IntegerField",
HookMetaService.MockIntegerField),
+ mock.patch("wtforms.fields.IntegerField",
HookMetaService.MockIntegerField),
+ mock.patch("wtforms.PasswordField",
HookMetaService.MockPasswordField),
+ mock.patch("wtforms.BooleanField",
HookMetaService.MockBooleanField),
+ mock.patch("wtforms.fields.BooleanField",
HookMetaService.MockBooleanField),
+ mock.patch("wtforms.fields.simple.BooleanField",
HookMetaService.MockBooleanField),
+ mock.patch("flask_babel.lazy_gettext", mock_lazy_gettext),
+ mock.patch("flask_appbuilder.fieldwidgets.BS3TextFieldWidget",
HookMetaService.MockAnyWidget),
+ mock.patch(
+ "flask_appbuilder.fieldwidgets.BS3TextAreaFieldWidget",
HookMetaService.MockAnyWidget
+ ),
+ mock.patch(
+ "flask_appbuilder.fieldwidgets.BS3PasswordFieldWidget",
HookMetaService.MockAnyWidget
+ ),
+ mock.patch("wtforms.validators.Optional",
HookMetaService.MockOptional),
+ mock.patch("wtforms.validators.any_of", mock_any_of),
+ # Prevent poisoning the global ProvidersManager singleton with
mocks
+
mock.patch("airflow.providers_manager.ProvidersManager._instance", None),
+
mock.patch("airflow.providers_manager.ProvidersManager.initialized",
return_value=False),
+ ):
+ pm = ProvidersManager()
+ return pm.hooks, pm.connection_form_widgets,
pm.field_behaviours # Will init providers hooks
+ else:
pm = ProvidersManager()
return pm.hooks, pm.connection_form_widgets, pm.field_behaviours
# Will init providers hooks
@@ -215,6 +233,45 @@ class HookMetaService:
elif isinstance(form_widget.field, HookMetaService.MockBaseField):
# legacy path, form widgets created using mocked WTForms
fields, need to convert to SerializedParam.dump()
hook_widgets[form_widget.field_name] =
form_widget.field.param.dump()
+ elif type(form_widget.field).__name__ == "UnboundField":
+ # handle real WTForms fields gracefully without needing mock
patches
+ field_class_name = getattr(form_widget.field.field_class,
"__name__", "")
+ param_type = "string"
+ param_format = None
+ if field_class_name == "BooleanField":
+ param_type = "boolean"
+ elif field_class_name == "IntegerField":
+ param_type = "integer"
+ elif field_class_name == "PasswordField":
+ param_format = "password"
+
+ label = (
+ form_widget.field.args[0]
+ if len(form_widget.field.args) > 0
+ else form_widget.field.kwargs.get("label")
+ )
+ validators = form_widget.field.kwargs.get("validators", [])
+ description = form_widget.field.kwargs.get("description", "")
+ default = form_widget.field.kwargs.get("default", None)
+
+ enum = {}
+ for v in validators:
+ if type(v).__name__ == "AnyOf":
+ enum["enum"] = getattr(v, "values", [])
+
+ types = [param_type, "null"]
+ format_dict = {"format": param_format} if param_format else {}
+
+ param = SerializedParam(
+ default=default,
+ title=str(label) if label is not None else None,
+ description=str(description) if description else None,
+ source=None,
+ type=types,
+ **format_dict,
+ **enum,
+ ).dump()
+ hook_widgets[form_widget.field_name] = param
else:
log.error("Unknown form widget in %s: %s", hook_key,
form_widget)
continue