This is an automated email from the ASF dual-hosted git repository.
eladkal pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-1-test by this push:
new 4bc9068b521 [v3-1-test] Fix `TypeError` crashes on `/users/list` and
`/roles/list` in FAB UI caused by concurrent API schema requests (#64156)
4bc9068b521 is described below
commit 4bc9068b521d6f26f5f49af07c2a42c90ff46cf9
Author: Subham <[email protected]>
AuthorDate: Wed Mar 25 12:42:32 2026 +0530
[v3-1-test] Fix `TypeError` crashes on `/users/list` and `/roles/list` in
FAB UI caused by concurrent API schema requests (#64156)
* 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
* Fix OpenAPI constraint generation by addressing Singleton patching,
restoring YAML extra fields logic, and safely resolving callable defaults in
Param object
---
.../core_api/services/ui/connections.py | 107 +++++++++++++++++----
1 file changed, 87 insertions(+), 20 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 1f2d8f659d3..ca295e880e2 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
@@ -154,23 +155,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.dict("airflow.utils.singleton.Singleton._instances", clear=True),
+
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
@@ -205,10 +223,59 @@ class HookMetaService:
result: dict[str, MutableMapping] = {}
for key, form_widget in form_widgets.items():
hook_key = key.split("__")[1]
- if isinstance(form_widget.field, HookMetaService.MockBaseField):
+ if isinstance(form_widget.field, dict):
+ hook_widgets = result.get(hook_key, {})
+ hook_widgets[form_widget.field_name] = form_widget.field
+ result[hook_key] = hook_widgets
+ elif isinstance(form_widget.field, HookMetaService.MockBaseField):
hook_widgets = result.get(hook_key, {})
hook_widgets[form_widget.field_name] =
form_widget.field.param.dump()
result[hook_key] = hook_widgets
+ 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)
+ if callable(default):
+ try:
+ default = default()
+ except Exception:
+ 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 = Param(
+ default=default,
+ title=str(label) if label is not None else None,
+ description=str(description) if description else None,
+ type=types,
+ **format_dict,
+ **enum,
+ ).dump()
+ hook_widgets = result.get(hook_key, {})
+ hook_widgets[form_widget.field_name] = param
+ result[hook_key] = hook_widgets
else:
log.error("Unknown form widget in %s: %s", hook_key,
form_widget)
return result