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 <noreply@anthropic.com>
This commit is contained in:
Jin 2026-09-24 22:00:16 -04:00 committed by GitHub
parent 1da284bb3a
commit e68b4e14e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 29 additions and 5 deletions

View File

@ -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:

View File

@ -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)),
)

View File

@ -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

View File

@ -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