fix(database): validate pool settings and command_timeout against booleans and non-finite values (#6009)

This commit is contained in:
Shxiao 2026-09-28 23:46:39 +09:00 committed by GitHub
parent 857eac45cd
commit b87ebd9402
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 0 deletions

View File

@ -327,6 +327,15 @@ This release closes that milestone with **181 merged pull requests**.
### Fixed
- **database:** `DatabaseConfig` now validates `pool_size`, `pool_recycle`, and
`command_timeout` strictly. Previously, YAML booleans (`true`/`false`) were
coerced to `1`/`0` respectively, allowing `pool_size: true` (pool size 1) and
`command_timeout: true` (a 1-second statement timeout) to silently pass
configuration loading. `pool_size` also accepted non-positive values (`0`,
`-1`), and `command_timeout` accepted `inf` (which never times out).
`pool_size` and `pool_recycle` now enforce positive integers, and
`command_timeout` rejects booleans and non-finite floats while retaining
`null` to disable timeouts.
- **frontend:** The optimistic human bubble keeps its quote and conversation
reference chips once a file upload finishes. The upload-complete update
replaced the bubble's `additional_kwargs` with only the uploaded files, so

View File

@ -332,6 +332,12 @@
### 修复
- **数据库:** `DatabaseConfig` 现在严格校验 `pool_size`、`pool_recycle` 与
`command_timeout`。此前,YAML 布尔值(`true`/`false`)会被强制转换为 `1`/`0`,
导致 `pool_size: true`(变成仅 1 个连接)和 `command_timeout: true`(变成 1 秒超时)
静默通过配置加载;`pool_size` 还接受非正数(`0`、`-1`),`command_timeout` 接受
`inf`(导致超时机制永不触发)。现在 `pool_size` 和 `pool_recycle` 强制要求正整数,
`command_timeout` 拒绝布尔值与非有限浮点数,同时保留 `null` 显式禁用超时。
- **前端:** 文件上传完成后,乐观显示的用户消息气泡不再丢失引用与对话
引用标签。上传完成时的更新会用仅含已上传文件的对象替换气泡的
`additional_kwargs`,因此在服务端回传该消息之前这些标签会消失;随本次

View File

@ -184,6 +184,7 @@ class DatabaseConfig(BaseModel):
)
pool_size: int = Field(
default=5,
gt=0,
description="Connection pool size for the app ORM engine (postgres only).",
)
pool_recycle: int = Field(
@ -194,6 +195,7 @@ class DatabaseConfig(BaseModel):
command_timeout: float | None = Field(
default=30,
gt=0,
allow_inf_nan=False,
description="Timeout in seconds for app ORM PostgreSQL commands. Set to null to disable the command timeout.",
)
postgres_schema: str = Field(
@ -213,6 +215,20 @@ class DatabaseConfig(BaseModel):
def _validate_postgres_schema(cls, value: str) -> str:
return validate_postgres_schema(value)
@field_validator("pool_size", "pool_recycle", mode="before")
@classmethod
def _reject_boolean_pool_settings(cls, value: object) -> object:
if isinstance(value, bool):
raise ValueError("must be an integer, not a boolean")
return value
@field_validator("command_timeout", mode="before")
@classmethod
def _reject_boolean_command_timeout(cls, value: object) -> object:
if isinstance(value, bool):
raise ValueError("command_timeout must be a number of seconds or null, not a boolean")
return value
# -- Legacy key migration (not user-configured) --
@model_validator(mode="before")

View File

@ -9,6 +9,8 @@ from types import ModuleType
from unittest.mock import AsyncMock, MagicMock, call, patch
import pytest
import yaml
from pydantic import ValidationError
from deerflow.config.database_config import DatabaseConfig
from deerflow.persistence import engine as engine_mod
@ -54,6 +56,62 @@ def test_postgres_engine_kwargs_allow_command_timeout_opt_out() -> None:
assert kwargs["connect_args"] == {}
@pytest.mark.parametrize(
("raw", "expected"),
[
(None, None),
(30, 30.0),
(0.5, 0.5),
("60", 60.0),
],
)
def test_database_command_timeout_accepts_finite_positive_seconds(raw, expected: float | None) -> None:
assert DatabaseConfig(command_timeout=raw).command_timeout == expected
@pytest.mark.parametrize(
"raw",
[
True,
False,
yaml.safe_load("on"),
yaml.safe_load("off"),
yaml.safe_load("yes"),
yaml.safe_load("no"),
0,
-1,
-0.5,
float("inf"),
float("-inf"),
float("nan"),
yaml.safe_load(".inf"),
yaml.safe_load(".nan"),
"1e999",
],
)
def test_database_command_timeout_rejects_boolean_and_non_finite_values(raw) -> None:
with pytest.raises(ValidationError, match="command_timeout"):
DatabaseConfig(command_timeout=raw)
@pytest.mark.parametrize(
("field", "invalid_value"),
[
("pool_size", True),
("pool_size", False),
("pool_size", 0),
("pool_size", -1),
("pool_recycle", True),
("pool_recycle", False),
("pool_recycle", 0),
("pool_recycle", -1),
],
)
def test_database_pool_settings_reject_booleans_and_non_positive_integers(field: str, invalid_value) -> None:
with pytest.raises(ValidationError):
DatabaseConfig(**{field: invalid_value})
@pytest.mark.asyncio
async def test_configured_command_timeout_ends_stalled_command(monkeypatch) -> None:
config = DatabaseConfig(