mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(cli): wire --http2/--no-http2 (HEADROOM_HTTP2) into proxy command (#1373)
## Description
The `headroom proxy` Click entrypoint (the command the `headroom`
console script dispatches to) never set `http2`, so `ProxyConfig` fell
back to its `http2=True` default and the upstream `httpx` client always
negotiated HTTP/2. The `HEADROOM_HTTP2` env var was only honored by the
legacy `server.py` `run()` path, leaving the Click command with no way
to force HTTP/1.1.
On a single shared proxy serving many concurrent Claude Code sessions,
HTTP/2 multiplexes every stream over one TLS connection. Frequent stream
cancellations (ESC, aborted tool calls, subagent cancels) can desync
that connection and surface as `ssl.SSLError: [SSL:
SSLV3_ALERT_BAD_RECORD_MAC]` on a later request. The traceback is
entirely within `httpcore/_async/http2.py`; the retry path does not
catch `SSLError`/`RemoteProtocolError`, so it leaks back to the client
as an API error.
This adds a `--http2/--no-http2` flag (default on,
`envvar=HEADROOM_HTTP2`) so operators can force HTTP/1.1 to avoid the
corruption. Default behavior is unchanged.
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Add a `--http2/--no-http2` Click option to the `proxy` command
(default `True`, `envvar="HEADROOM_HTTP2"`), mirroring the adjacent
`--max-keepalive` option.
- Add the `http2: bool` parameter to the `proxy()` signature.
- Pass it through to `ProxyConfig(http2=http2)` so the env/flag actually
reaches the upstream `httpx` client. (`ProxyConfig.http2` already
existed and is read at client construction; it was simply never wired
from this entrypoint.)
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ git show fix/cli-http2-flag:headroom/cli/proxy.py | uv run ruff check --stdin-filename headroom/cli/proxy.py -
All checks passed!
$ uv run headroom proxy --help
...
--http2 / --no-http2 Use HTTP/2 to upstream providers (default:
on, env: HEADROOM_HTTP2). Disable to force
HTTP/1.1, which avoids shared-connection TLS
corruption (SSLV3_ALERT_BAD_RECORD_MAC) when
many concurrent streams are cancelled.
...
(exit code 0)
```
## Real Behavior Proof
- Environment: macOS, Python 3.12, `headroom-ai` built from this branch
in a uv-managed venv.
- Exact command / steps: `uv run headroom proxy --help`; `ruff check`
against the branch content via stdin.
- Observed result: the `--http2/--no-http2` flag renders in `--help` and
the command builds with exit code 0 (confirming the Click option,
function signature, and `ProxyConfig` kwarg all line up); `ruff` reports
`All checks passed!`.
- Not tested: full `pytest` suite and `mypy` (single-file CLI plumbing
change); live HTTP/1.1 negotiation against an upstream provider with
`--no-http2` was not exercised end-to-end in CI.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — CLI-only change.
## Additional Notes
- No dedicated test added: this is a one-line passthrough of an
already-existing `ProxyConfig.http2` field, mirroring the neighboring
`--max-keepalive` option which is likewise wired without a per-flag
test. The `--help` render confirms the option/signature/kwarg wiring.
Happy to add a regression test asserting `--no-http2` yields
`config.http2 is False` if maintainers prefer.
- Documentation/CHANGELOG left unchecked: the flag is self-documenting
via `--help`; point me at the right doc/changelog entry if one is
expected.
- The fix is also what unblocks downstream consumers that set
`HEADROOM_HTTP2=false` expecting it to be honored by the `headroom
proxy` entrypoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bb3e040a46
commit
e06b61671f
1 changed files with 13 additions and 0 deletions
|
|
@ -177,6 +177,17 @@ def dashboard(port: int, no_open: bool) -> None:
|
|||
envvar="HEADROOM_MAX_KEEPALIVE",
|
||||
help="Maximum upstream keep-alive connections (default: 100, env: HEADROOM_MAX_KEEPALIVE)",
|
||||
)
|
||||
@click.option(
|
||||
"--http2/--no-http2",
|
||||
"http2",
|
||||
default=True,
|
||||
envvar="HEADROOM_HTTP2",
|
||||
help=(
|
||||
"Use HTTP/2 to upstream providers (default: on, env: HEADROOM_HTTP2). "
|
||||
"Disable to force HTTP/1.1, which avoids shared-connection TLS corruption "
|
||||
"(SSLV3_ALERT_BAD_RECORD_MAC) when many concurrent streams are cancelled."
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--keepalive-expiry",
|
||||
"keepalive_expiry",
|
||||
|
|
@ -791,6 +802,7 @@ def proxy(
|
|||
max_connections: int,
|
||||
max_keepalive_connections: int,
|
||||
keepalive_expiry: float,
|
||||
http2: bool,
|
||||
intercept_tool_results: bool,
|
||||
no_optimize: bool,
|
||||
no_cache: bool,
|
||||
|
|
@ -1041,6 +1053,7 @@ def proxy(
|
|||
max_connections=max_connections,
|
||||
max_keepalive_connections=max_keepalive_connections,
|
||||
keepalive_expiry=keepalive_expiry,
|
||||
http2=http2,
|
||||
log_file=None if is_stateless else log_file,
|
||||
log_full_messages=log_messages
|
||||
or os.environ.get("HEADROOM_LOG_MESSAGES", "").lower() in ("true", "1", "yes", "on"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue