From e68b4e14e4ce30673fa5c2721391d3483616f9a6 Mon Sep 17 00:00:00 2001 From: Jin Date: Thu, 24 Sep 2026 22:00:16 -0400 Subject: [PATCH] fix(gateway): treat a blank GATEWAY_WORKERS and Lark broker port/timeout as unset (#5840) Follows #5801 for the remaining numeric env reads that break on an empty value (`-e VAR=`, or `${VAR}` with VAR unset): - The WeChat QR worker guard read a blank GATEWAY_WORKERS as an invalid count and refused QR login, although the compose command starts one worker for it (`--workers ${GATEWAY_WORKERS:-1}`) and the other worker checks treat it as 1. A blank value now falls through to WEB_CONCURRENCY and then 1, so the guard still fails closed for real multi-worker setups. - The Lark broker aborted startup on int("") for a blank DEERFLOW_LARK_BROKER_PORT or DEERFLOW_LARK_BROKER_TIMEOUT; it now uses the defaults. Co-authored-by: Claude Opus 5.5 --- backend/app/gateway/routers/channel_connections.py | 3 ++- .../harness/deerflow/integrations/lark_broker.py | 5 +++-- backend/tests/test_channel_connections_router.py | 14 ++++++++++++-- backend/tests/test_lark_broker.py | 12 ++++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/backend/app/gateway/routers/channel_connections.py b/backend/app/gateway/routers/channel_connections.py index 0733ff2890..ef25aa4545 100644 --- a/backend/app/gateway/routers/channel_connections.py +++ b/backend/app/gateway/routers/channel_connections.py @@ -747,8 +747,9 @@ async def _require_wechat_qr_login(request: Request) -> ChannelConnectionsConfig # QR sessions and mutation locks live in one process. Reject every QR route # before session access when requests could land on different workers. # WEB_CONCURRENCY is Uvicorn's fallback when no worker count is supplied. + # A blank value means "unset", as the compose command's ${GATEWAY_WORKERS:-1} treats it. try: - workers = int(os.environ.get("GATEWAY_WORKERS", os.environ.get("WEB_CONCURRENCY", "1"))) + workers = int(os.environ.get("GATEWAY_WORKERS") or os.environ.get("WEB_CONCURRENCY") or "1") except ValueError: workers = 0 if workers != 1: diff --git a/backend/packages/harness/deerflow/integrations/lark_broker.py b/backend/packages/harness/deerflow/integrations/lark_broker.py index 63c2d5a187..35dfa17c76 100644 --- a/backend/packages/harness/deerflow/integrations/lark_broker.py +++ b/backend/packages/harness/deerflow/integrations/lark_broker.py @@ -430,8 +430,9 @@ def _config_from_env() -> BrokerConfig: config_dir=os.environ.get("LARKSUITE_CLI_CONFIG_DIR", "/var/lark/config"), data_dir=os.environ.get("LARKSUITE_CLI_DATA_DIR", "/var/lark/data"), host=os.environ.get("DEERFLOW_LARK_BROKER_HOST", LARK_BROKER_DEFAULT_HOST), - port=int(os.environ.get("DEERFLOW_LARK_BROKER_PORT", str(LARK_BROKER_DEFAULT_PORT))), - timeout_seconds=int(os.environ.get("DEERFLOW_LARK_BROKER_TIMEOUT", str(LARK_BROKER_DEFAULT_TIMEOUT_SECONDS))), + # A blank value means "unset" (`-e DEERFLOW_LARK_BROKER_PORT=`); int("") would abort startup. + port=int(os.environ.get("DEERFLOW_LARK_BROKER_PORT") or LARK_BROKER_DEFAULT_PORT), + timeout_seconds=int(os.environ.get("DEERFLOW_LARK_BROKER_TIMEOUT") or LARK_BROKER_DEFAULT_TIMEOUT_SECONDS), deny_subcommands=parse_deny_subcommands(os.environ.get(LARK_BROKER_DENY_SUBCOMMANDS_ENV)), ) diff --git a/backend/tests/test_channel_connections_router.py b/backend/tests/test_channel_connections_router.py index 304eab7289..e48f008083 100644 --- a/backend/tests/test_channel_connections_router.py +++ b/backend/tests/test_channel_connections_router.py @@ -1226,7 +1226,7 @@ def test_disconnect_connection_is_current_user_scoped(tmp_path): anyio.run(repo.close) -@pytest.mark.parametrize("worker_env", [{}, {"GATEWAY_WORKERS": "1"}, {"WEB_CONCURRENCY": "1"}]) +@pytest.mark.parametrize("worker_env", [{}, {"GATEWAY_WORKERS": "1"}, {"WEB_CONCURRENCY": "1"}, {"GATEWAY_WORKERS": ""}]) def test_wechat_qr_login_saves_credentials_without_exposing_token(monkeypatch, worker_env): from app.channels.wechat_qr_login import WechatQRLogin @@ -1366,7 +1366,17 @@ def test_wechat_qr_confirmation_preserves_bot_id_unless_replaced(monkeypatch, re assert saved == expected -@pytest.mark.parametrize("worker_env", [{"GATEWAY_WORKERS": "2"}, {"GATEWAY_WORKERS": "4"}, {"WEB_CONCURRENCY": "2"}, {"GATEWAY_WORKERS": "invalid"}]) +@pytest.mark.parametrize( + "worker_env", + [ + {"GATEWAY_WORKERS": "2"}, + {"GATEWAY_WORKERS": "4"}, + {"WEB_CONCURRENCY": "2"}, + {"GATEWAY_WORKERS": "invalid"}, + # A blank GATEWAY_WORKERS is unset, so WEB_CONCURRENCY still decides. + {"GATEWAY_WORKERS": "", "WEB_CONCURRENCY": "2"}, + ], +) @pytest.mark.parametrize("method,suffix", [("POST", ""), ("POST", "/session/poll"), ("DELETE", "/session")]) def test_wechat_qr_routes_reject_unsupported_workers_before_session_access(monkeypatch, worker_env, method, suffix): from app.channels.wechat_qr_login import WechatQRLogin diff --git a/backend/tests/test_lark_broker.py b/backend/tests/test_lark_broker.py index 83e4c54820..77f557b380 100644 --- a/backend/tests/test_lark_broker.py +++ b/backend/tests/test_lark_broker.py @@ -355,3 +355,15 @@ def test_allowed_subcommand_still_runs_with_denylist(tmp_path: Path) -> None: assert result.exit_code == 0 payload = json.loads(result.stdout.decode()) assert payload["argv"] == ["auth", "status"] + + +@pytest.mark.parametrize("name", ["DEERFLOW_LARK_BROKER_PORT", "DEERFLOW_LARK_BROKER_TIMEOUT"]) +def test_config_from_env_treats_blank_numbers_as_unset(monkeypatch, name) -> None: + monkeypatch.delenv("DEERFLOW_LARK_BROKER_PORT", raising=False) + monkeypatch.delenv("DEERFLOW_LARK_BROKER_TIMEOUT", raising=False) + monkeypatch.setenv(name, "") + + config = lark_broker._config_from_env() + + assert config.port == lark_broker.LARK_BROKER_DEFAULT_PORT + assert config.timeout_seconds == lark_broker.LARK_BROKER_DEFAULT_TIMEOUT_SECONDS