diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 12971b9b8ca..5897698d1c8 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -11,6 +11,7 @@ from pydantic import ( PositiveFloat, PositiveInt, computed_field, + field_validator, ) from pydantic_settings import BaseSettings @@ -280,6 +281,27 @@ class PluginConfig(BaseSettings): default="", ) + @field_validator("PLUGIN_REMOTE_INSTALL_PORT", mode="before") + @classmethod + def _reject_host_port_shaped_plugin_remote_install_port(cls, v): + """Reject ``host:port``-shaped values with an actionable hint. + + ``EXPOSE_PLUGIN_DEBUGGING_PORT`` is overloaded: it feeds both the + plugin_daemon ``ports:`` mapping (where ``127.0.0.1:5003`` is valid + compose syntax) and this integer app setting advertised in the console. + Without this guard a loopback bind spec crashloops the api container + with an opaque ``int_parsing`` traceback. See issue #39323. + """ + if isinstance(v, str) and ":" in v.strip(): + raise ValueError( + "PLUGIN_REMOTE_INSTALL_PORT must be a bare port number, got " + f"{v!r}. A 'host:port' value usually means " + "EXPOSE_PLUGIN_DEBUGGING_PORT was set to a compose publish spec " + "like '127.0.0.1:5003'; bind loopback via a " + "docker-compose.override.yaml instead of overloading this var." + ) + return v + @property def NEW_USER_DEFAULT_PLUGIN_ID_LIST(self) -> list[str]: return [item.strip() for item in self.NEW_USER_DEFAULT_PLUGIN_IDS.split(",") if item.strip()] diff --git a/api/tests/unit_tests/configs/test_dify_config.py b/api/tests/unit_tests/configs/test_dify_config.py index 49934cf4837..3588f6beef1 100644 --- a/api/tests/unit_tests/configs/test_dify_config.py +++ b/api/tests/unit_tests/configs/test_dify_config.py @@ -109,6 +109,24 @@ def test_new_user_default_plugin_ids_are_parsed_from_env(monkeypatch: pytest.Mon ] +def test_plugin_remote_install_port_rejects_host_port_spec(monkeypatch: pytest.MonkeyPatch) -> None: + """A 'host:port' compose publish spec must produce an actionable error, not an opaque int_parsing traceback.""" + _set_basic_config_env(monkeypatch) + monkeypatch.setenv("PLUGIN_REMOTE_INSTALL_PORT", "127.0.0.1:5003") + + with pytest.raises(ValueError, match="must be a bare port number"): + DifyConfig(_env_file=None) + + +def test_plugin_remote_install_port_accepts_bare_port(monkeypatch: pytest.MonkeyPatch) -> None: + _set_basic_config_env(monkeypatch) + monkeypatch.setenv("PLUGIN_REMOTE_INSTALL_PORT", "5003") + + config = DifyConfig(_env_file=None) + + assert config.PLUGIN_REMOTE_INSTALL_PORT == 5003 + + def test_new_user_default_models_are_parsed_from_env(monkeypatch: pytest.MonkeyPatch) -> None: _set_basic_config_env(monkeypatch) monkeypatch.setenv(