Skip to content

fix(config): bound OCTOP_PORT and --port to a bindable range - #1150

Merged
jubaoliang merged 2 commits into
TencentCloud:developfrom
Lesereingrape:fix/env-port-range-guard
Sep 26, 2026
Merged

jubaoliang merged 2 commits into
TencentCloud:developfrom
Lesereingrape:fix/env-port-range-guard

Conversation

@Lesereingrape

Copy link
Copy Markdown
Contributor

Summary

OCTOP_PORT and octop run --port accept any integer, so a port no socket can ever bind — 70000, -1 — is treated as a valid override. It then outranks a perfectly good config.json port and kills startup inside socket.bind:

>>> socket.socket().bind(("127.0.0.1", 70000))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OverflowError: bind(): port must be 0-65535.

The traceback names neither the key nor the value, and on the CLI path the damage is sticky:

  • env — env_bind_overrides() (src/octop/config.py:403-408) parses with bare int(). Its docstring promises that "an invalid OCTOP_PORT logs a warning and yields no override, so the file/default value wins", but only a non-numeric value reaches that branch. 70000 parses fine, wins the config.json comparison in resolve_bind() (src/octop/cli/commands/run.py:104) and in load_config() (src/octop/config.py:462-465), and the server never starts.
  • flag — --port is type=int with no range, and octop run persists the CLI override to config.json before uvicorn is called (src/octop/cli/commands/run.py:180-182; test_run_writes_config_even_on_uvicorn_failure locks in that ordering on purpose). So one typo'd port is saved, and every later octop run inherits the unusable value.

Measured on pristine develop @ 1d2b2558 with only the new tests added:

tests/unit/cli/test_run_cmd.py::test_run_rejects_unbindable_port_before_saving_config FAILED
>       assert "is not in the range" in _all_output(r)
E       AssertionError: assert 'is not in the range' in 'Saved config to ...\\config.json\n'

tests/unit/cli/test_run_bind_resolution.py::test_out_of_range_env_port_falls_back_to_file[70000] FAILED
tests/unit/cli/test_run_bind_resolution.py::test_out_of_range_env_port_falls_back_to_file[-1] FAILED
E       assert (None, -1) == (None, 8088)

i.e. the baseline prints Saved config to … and then starts uvicorn with the unbindable port.

Changes Made

  • src/octop/config.py — an out-of-range OCTOP_PORT is dropped with the same logger.warning(…, "ignoring override") shape the function already uses for a non-numeric value, so the file/default port wins as documented.
  • src/octop/cli/commands/run.py — --port becomes click.IntRange(0, 65535) (the idiom already used at src/octop/cli/commands/memory.py:145), so a bad flag is a Click usage error that never reaches config.json.
  • tests/unit/cli/test_run_bind_resolution.py — out-of-range env values fall back to the file port; OCTOP_PORT=0 still means "OS-assigned".
  • tests/unit/cli/test_run_cmd.py — --port 70000 / --port -1 exit non-zero, name the range, and leave no config.json behind; --port 0 still reaches uvicorn as 0.
  • CHANGELOG.md (### 修复 under [Unreleased]), plus the two doc cells that state the accepted values (docs/configuration.md, docs/cli.md — --help now renders INTEGER RANGE … [0<=x<=65535]).

The two entry points fail differently on purpose: the env override already has a documented fallback contract, while a flag the operator typed explicitly must not silently start the server on a different port than requested — that one should say so and stop.

0 stays in range deliberately. resolve_bind() reads port=0 as "let the OS pick a free port" (src/octop/cli/commands/run.py:102-103), so a 1-65535 bound would break a documented feature; the control tests above pin both paths.

Not touched: _coerce_int() (src/octop/config.py:274) still accepts any int for keys such as OCTOP_DATABASE_PORT — those talk to another host and fail in their own way, so bounding them is a separate question; and a port written by hand into config.json is still honoured as-is rather than second-guessed at startup.

Target branch

  • Base is develop (feature / fix — default)
  • Base is main (release/* or hotfix/* only)

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation (two table cells describing the accepted range)
  • Refactor / chore
  • Release / hotfix

Test plan

Windows 11, Python 3.12.11, branch fix/env-port-range-guard off origin/develop @ 1d2b2558:

  • ruff check src tests → All checks passed!; ruff format --check src tests → 1062 files already formatted (ruff 0.15.20 from the project venv)
  • mypy src/octop → Success: no issues found in 519 source files (strict = true comes from pyproject.toml, so this is make typecheck)
  • pytest tests/unit/cli -m "not live" → 93 passed, 9 skipped — this covers both touched test files and their neighbours
  • pytest -n auto -m "not live" — the whole non-live suite, i.e. make test's command → 19 failed, 3651 passed, 118 skipped in 449.74s (started at ea10de20; the one commit made after it started, 410645e8, edits two markdown doc cells)
  • The 19 failures above are pre-existing on develop, not caused by this change: the same two directories on a pristine 1d2b2558 worktree fail with the same 19 node ids (pytest -n auto tests/unit/db tests/unit/backup -m "not live" → 19 failed, 190 passed, 2 skipped), and none of the files this PR touches are imported by them.

make all was not run as a single command: make is not available in this environment. Its steps were run individually as listed above (ruff check + format check, mypy, pytest); make all's remaining steps (Prettier / dashboard build) are untouched by this diff, which contains no dashboard/ change.

  • Added/updated tests
  • make all passes locally — see the note above: the equivalent gate commands pass, the make wrapper itself was not run

Checklist

  • Updated CHANGELOG.md (if user-facing)
  • README / docs updated (if needed) — docs/configuration.md + docs/cli.md only
  • Base is develop, branch contains only this fix (2 commits: the fix + the doc cells)

Who did this work: the analysis, the patch and the tests were written by an agent (Qoder CLI) working for Lesereingrape, and the red/green numbers above were measured by that agent in one session. Nothing here has been read by a human on our side yet, so treat the diff as agent-authored until someone says otherwise.

OCTOP_PORT 只按 int() 解析、octop run --port 也只有 type=int,70000/-1
这类根本无法绑定的取值会盖掉 config.json 里本来可用的端口,进程死在
socket.bind 抛出的 OverflowError 里,报错既不含变量名也看不出端口来源;
--port 更是在启动之前就把该值写进 config.json,之后每次 octop run 都继续继承。

现按 resolve_bind 已经支持的「0 表示由系统随机分配端口」把取值收口到
0-65535:环境变量越界沿用「告警 + 不覆盖」的既有契约回落配置文件端口,
命令行 --port 越界交给 click.IntRange 在使用期错误里拦下、不落盘。
`octop run --port` 现在会渲染成 `INTEGER RANGE … [0<=x<=65535]`,环境变量表也顺手写明
`OCTOP_PORT` 接受 0–65535(0 = 由系统随机分配),与 `OCTOP_LOG_LEVEL` 那行「One of …」的写法一致。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants