headroom/tests/test_proxy
Tejas Chopra 2976d49f18
fix(proxy): preserve sub-path in X-Headroom-Base-Url custom upstream (#2037) (#2127)
## Description

`_resolve_openai_upstream_base` ran the `X-Headroom-Base-Url` value
through `_normalize_origin`, which strips the path. A custom
OpenAI-compatible upstream served from a sub-path, such as
`https://host/api/v1`, was routed to the bare origin and returned
`proxy_error` (#2037). This re-attaches the path after origin
normalization.

This is a clean extraction of the path fix from #2047, which bundled it
with an unrelated `supports_websockets = true` to `false` default change
across init/wrap/codex. #2047 can be closed in favor of this narrower
fix.

Closes #2037

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

- `headroom/proxy/handlers/openai.py`: re-attach the request header path
component in `_resolve_openai_upstream_base` after origin normalization.
- `tests/test_proxy/test_openai_upstream_header.py`: assert sub-paths
are preserved and trailing slashes are normalized.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_proxy/test_openai_upstream_header.py
5 passed

$ ruff check headroom/proxy/handlers/openai.py tests/test_proxy/test_openai_upstream_header.py
All checks passed!
```

## Real Behavior Proof

- Environment: local proxy header resolution path, custom
OpenAI-compatible upstream configured via `X-Headroom-Base-Url`.
- Exact command / steps: resolve `X-Headroom-Base-Url:
https://gateway.example/api/v1` through `_resolve_openai_upstream_base`
/ `_resolve_openai_upstream`.
- Observed result: before the fix, the upstream resolved to
`https://gateway.example` and lost `/api/v1`, causing the proxy to route
to the wrong endpoint. After the fix, it resolves to
`https://gateway.example/api/v1`; a trailing slash is normalized away.
- Not tested: end-to-end request against a live third-party
OpenAI-compatible gateway. The regression is covered at the proxy
routing helper layer where the path was dropped.

## 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
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable

## Screenshots (if applicable)

N/A.

## Additional Notes

- Documentation is not updated because this fixes the existing header
behavior rather than changing a documented user-facing contract.
- Changelog is not updated in this PR; the change is scoped to the
regression and test.
- `mypy headroom` was not run in the author's workflow.
2026-07-13 19:53:45 -04:00
..
test_anthropic_buffered_timeout.py fix(proxy): add an Anthropic buffered read-timeout override (#1331) 2026-06-23 22:46:31 -05:00
test_anthropic_ccr_deferred_injection.py feat(cache): provider-agnostic cache-mode delta + cc-agnostic prefix comparison (#1868) 2026-07-08 13:29:35 -07:00
test_anthropic_ccr_raise.py fix(anthropic): CCR exception must re-raise, not silently swallow (#838) 2026-06-10 21:11:46 -05:00
test_anthropic_streaming_ccr_retrieve.py fix(proxy): handle streaming CCR retrieval (#1451) 2026-06-30 13:46:34 -05:00
test_anthropic_upstream_header.py fix(proxy): honor x-headroom-base-url on /v1/messages route (#1763) 2026-07-09 12:43:40 -05:00
test_background_compression.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_bedrock_passthrough.py feat(proxy): compress AWS Bedrock InvokeModel requests via configurable upstream (#720) 2026-06-16 14:56:16 -05:00
test_cc_switch_reconciler.py feat(proxy): cc-switch reconciler — keep Headroom in the request path alongside cc-switch (#1030) 2026-06-17 08:41:16 -05:00
test_ccr_frozen_prefix_coupling.py fix(proxy): stop re-compressing headroom_retrieve output and emitting unredeemable markers (#1323) 2026-06-25 10:11:42 -05:00
test_compression_failure_action.py fix(codex): fail open for proxy compression timeout 2026-06-04 14:04:49 +05:30
test_compression_timeout_config.py feat(proxy): make COMPRESSION_TIMEOUT_SECONDS configurable via env (#946) (#991) 2026-06-16 21:10:01 -05:00
test_compute_turn_id.py fix(proxy): strip cache_control before hashing turn_id 2026-04-23 16:04:57 +02:00
test_gemini_savings_profile.py fix(proxy/gemini): thread savings-profile kwargs into apply() (#1994) 2026-07-12 13:54:44 -04:00
test_header_safe_transforms.py feat(transforms): attribute read_lifecycle + smart_crush tags (#249) 2026-06-11 11:51:26 -05:00
test_mcp_stats_aggregation.py fix(proxy): Strands MCP bundle + backend path fixes + Codex fail-closed protection 2026-05-21 11:00:14 -07:00
test_openai_backend_path.py fix(proxy): Strands MCP bundle + backend path fixes + Codex fail-closed protection 2026-05-21 11:00:14 -07:00
test_openai_chat_savings_profile.py fix(proxy/openai): thread savings-profile kwargs into chat completions (#1606) 2026-07-07 11:27:54 -05:00
test_openai_responses_ccr.py feat(ccr): wire retrieve-tool interception into OpenAI Responses handler (#1898) 2026-07-09 09:41:06 -04:00
test_openai_stream_usage_option.py fix(proxy/openai): respect explicit stream_options.include_usage (#2026) 2026-07-13 00:43:16 -04:00
test_openai_transport_path_prefix.py fix(codex): OpenCode Zen telemetry attribution (#1648) 2026-07-07 11:35:21 -05:00
test_openai_upstream_header.py fix(proxy): preserve sub-path in X-Headroom-Base-Url custom upstream (#2037) (#2127) 2026-07-13 19:53:45 -04:00
test_phase3_byte_identity.py perf(compression): take large cold-start contexts off the synchronous kompress path (#1171) (#1298) 2026-06-23 10:48:06 -05:00
test_request_logger.py feat(proxy): log compressed messages alongside original request (#261) 2026-06-11 19:02:54 -05:00
test_transformations_feed.py fix(proxy): scope CORS to loopback + gate operator/content endpoints (#1226) 2026-06-21 00:50:55 -07:00