Skip to content

Commit 25ef386

Browse files
rustyconoverclaude
andcommitted
Expose WWW-Authenticate and X-Request-ID in CORS headers (v0.6.2)
Browsers cannot read non-safelisted response headers from cross-origin responses unless they appear in Access-Control-Expose-Headers. Always expose WWW-Authenticate (needed for OAuth discovery from 401 responses) and X-Request-ID (for client-side debugging) when CORS is enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff6f995 commit 25ef386

4 files changed

Lines changed: 22 additions & 6 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vgi-rpc"
3-
version = "0.6.1"
3+
version = "0.6.2"
44
description = "Vector Gateway Interface - RPC framework based on Apache Arrow"
55
readme = "README.md"
66
requires-python = ">=3.13"

tests/test_http.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,16 @@ def test_cors_specific_origin(self) -> None:
11141114
resp = tc.simulate_options("/add", headers={"Origin": "http://example.com"})
11151115
assert resp.headers.get("access-control-allow-origin") == "http://example.com"
11161116

1117+
def test_cors_exposes_www_authenticate_and_request_id(self) -> None:
1118+
"""CORS always exposes WWW-Authenticate and X-Request-ID headers."""
1119+
server = RpcServer(RpcFixtureService, RpcFixtureServiceImpl())
1120+
app = make_wsgi_app(server, signing_key=b"test", cors_origins="*")
1121+
tc = falcon.testing.TestClient(app)
1122+
resp = tc.simulate_options("/add", headers={"Origin": "http://example.com"})
1123+
expose = resp.headers.get("access-control-expose-headers", "")
1124+
assert "WWW-Authenticate" in expose
1125+
assert "X-Request-ID" in expose
1126+
11171127
def test_no_cors_by_default(self) -> None:
11181128
"""Without cors_origins, no CORS headers are added."""
11191129
server = RpcServer(RpcFixtureService, RpcFixtureServiceImpl())

uv.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vgi_rpc/http/_server.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,7 +2132,9 @@ def make_wsgi_app(
21322132
if otel_config is not None:
21332133
middleware.append(_OtelFalconMiddleware())
21342134

2135-
cors_expose: list[str] = []
2135+
# Always expose auth and request-id headers; capability headers are
2136+
# appended conditionally below.
2137+
cors_expose: list[str] = ["WWW-Authenticate", _REQUEST_ID_HEADER]
21362138

21372139
# Build capability headers
21382140
capability_headers: dict[str, str] = {}
@@ -2162,9 +2164,10 @@ def make_wsgi_app(
21622164
www_authenticate = _build_www_authenticate(_validated_oauth_metadata, prefix)
21632165

21642166
if cors_origins is not None:
2165-
cors_kwargs: dict[str, Any] = {"allow_origins": cors_origins}
2166-
if cors_expose:
2167-
cors_kwargs["expose_headers"] = cors_expose
2167+
cors_kwargs: dict[str, Any] = {
2168+
"allow_origins": cors_origins,
2169+
"expose_headers": cors_expose,
2170+
}
21682171
middleware.append(falcon.CORSMiddleware(**cors_kwargs))
21692172
if authenticate is not None:
21702173
on_auth_failure: Callable[[str | None, str], None] | None = None

0 commit comments

Comments
 (0)