Skip to content

Commit 7e69bb2

Browse files
rustyconoverclaude
andcommitted
Add Access-Control-Max-Age header on CORS preflight and improve external logging (v0.6.4)
- Add cors_max_age parameter to make_wsgi_app() (default 7200s / 2 hours) - New _CorsMaxAgeMiddleware sets Access-Control-Max-Age on OPTIONS responses - Fix external.py SHA-256 docstring (pre-compression, not post-compression) - Improve externalize logging to show both raw and uploaded byte sizes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f6dd979 commit 7e69bb2

4 files changed

Lines changed: 80 additions & 7 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.3"
3+
version = "0.6.4"
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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,38 @@ def test_no_cors_by_default(self) -> None:
11331133
resp = tc.simulate_options("/add", headers={"Origin": "http://example.com"})
11341134
assert "access-control-allow-origin" not in resp.headers
11351135

1136+
def test_cors_max_age_default(self) -> None:
1137+
"""OPTIONS responses include Access-Control-Max-Age: 7200 by default."""
1138+
server = RpcServer(RpcFixtureService, RpcFixtureServiceImpl())
1139+
app = make_wsgi_app(server, signing_key=b"test", cors_origins="*")
1140+
tc = falcon.testing.TestClient(app)
1141+
resp = tc.simulate_options("/add", headers={"Origin": "http://example.com"})
1142+
assert resp.headers.get("access-control-max-age") == "7200"
1143+
1144+
def test_cors_max_age_custom(self) -> None:
1145+
"""cors_max_age overrides the default value."""
1146+
server = RpcServer(RpcFixtureService, RpcFixtureServiceImpl())
1147+
app = make_wsgi_app(server, signing_key=b"test", cors_origins="*", cors_max_age=3600)
1148+
tc = falcon.testing.TestClient(app)
1149+
resp = tc.simulate_options("/add", headers={"Origin": "http://example.com"})
1150+
assert resp.headers.get("access-control-max-age") == "3600"
1151+
1152+
def test_cors_max_age_none_omits_header(self) -> None:
1153+
"""cors_max_age=None omits the Access-Control-Max-Age header."""
1154+
server = RpcServer(RpcFixtureService, RpcFixtureServiceImpl())
1155+
app = make_wsgi_app(server, signing_key=b"test", cors_origins="*", cors_max_age=None)
1156+
tc = falcon.testing.TestClient(app)
1157+
resp = tc.simulate_options("/add", headers={"Origin": "http://example.com"})
1158+
assert "access-control-max-age" not in resp.headers
1159+
1160+
def test_cors_max_age_not_on_post(self) -> None:
1161+
"""Access-Control-Max-Age is only set on OPTIONS, not regular requests."""
1162+
server = RpcServer(RpcFixtureService, RpcFixtureServiceImpl())
1163+
app = make_wsgi_app(server, signing_key=b"test", cors_origins="*")
1164+
tc = falcon.testing.TestClient(app)
1165+
resp = tc.simulate_get("/", headers={"Origin": "http://example.com"})
1166+
assert "access-control-max-age" not in resp.headers
1167+
11361168

11371169
# ---------------------------------------------------------------------------
11381170
# Tests: Max request bytes header

vgi_rpc/external.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,8 @@ def make_external_location_batch(
387387
Args:
388388
schema: The schema the pointer batch should conform to.
389389
url: The URL where the actual data resides.
390-
sha256: Optional hex-encoded SHA-256 of the stored bytes
391-
(post-compression). Included as ``vgi_rpc.location.sha256``
390+
sha256: Optional hex-encoded SHA-256 of the raw IPC bytes
391+
(pre-compression). Included as ``vgi_rpc.location.sha256``
392392
metadata so consumers can verify data integrity on fetch.
393393
394394
Returns:
@@ -651,16 +651,23 @@ def maybe_externalize_collector(
651651
ipc_bytes = zstandard.ZstdCompressor(level=config.compression.level).compress(ipc_bytes)
652652
content_encoding = config.compression.algorithm
653653

654+
raw_size = original_bytes if original_bytes is not None else len(ipc_bytes)
654655
url = _traced_upload(
655656
ipc_bytes, out.output_schema, config.storage, content_encoding=content_encoding, original_bytes=original_bytes
656657
)
657658
_logger.debug(
658-
"Batch externalized: %s (%d bytes, compressed=%s, sha256=%s)",
659+
"Batch externalized: %s (%d bytes raw, %d bytes uploaded, compressed=%s, sha256=%s)",
659660
url,
661+
raw_size,
660662
len(ipc_bytes),
661663
content_encoding is not None,
662664
data_sha256,
663-
extra={"url": url, "size_bytes": len(ipc_bytes), "compressed": content_encoding is not None},
665+
extra={
666+
"url": url,
667+
"raw_size_bytes": raw_size,
668+
"uploaded_size_bytes": len(ipc_bytes),
669+
"compressed": content_encoding is not None,
670+
},
664671
)
665672

666673
pointer_batch, pointer_cm = make_external_location_batch(out.output_schema, url, sha256=data_sha256)
@@ -724,16 +731,23 @@ def maybe_externalize_batch(
724731
ipc_bytes = zstandard.ZstdCompressor(level=config.compression.level).compress(ipc_bytes)
725732
content_encoding = config.compression.algorithm
726733

734+
raw_size = original_bytes if original_bytes is not None else len(ipc_bytes)
727735
url = _traced_upload(
728736
ipc_bytes, batch.schema, config.storage, content_encoding=content_encoding, original_bytes=original_bytes
729737
)
730738
_logger.debug(
731-
"Batch externalized: %s (%d bytes, compressed=%s, sha256=%s)",
739+
"Batch externalized: %s (%d bytes raw, %d bytes uploaded, compressed=%s, sha256=%s)",
732740
url,
741+
raw_size,
733742
len(ipc_bytes),
734743
content_encoding is not None,
735744
data_sha256,
736-
extra={"url": url, "size_bytes": len(ipc_bytes), "compressed": content_encoding is not None},
745+
extra={
746+
"url": url,
747+
"raw_size_bytes": raw_size,
748+
"uploaded_size_bytes": len(ipc_bytes),
749+
"compressed": content_encoding is not None,
750+
},
737751
)
738752

739753
return make_external_location_batch(batch.schema, url, sha256=data_sha256)

vgi_rpc/http/_server.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,6 +1958,26 @@ def process_response(
19581958
resp.set_header("Content-Encoding", "zstd")
19591959

19601960

1961+
class _CorsMaxAgeMiddleware:
1962+
"""Falcon middleware that sets ``Access-Control-Max-Age`` on OPTIONS responses."""
1963+
1964+
__slots__ = ("_max_age",)
1965+
1966+
def __init__(self, max_age: int) -> None:
1967+
self._max_age = str(max_age)
1968+
1969+
def process_response(
1970+
self,
1971+
req: falcon.Request,
1972+
resp: falcon.Response,
1973+
resource: object,
1974+
req_succeeded: bool,
1975+
) -> None:
1976+
"""Set Access-Control-Max-Age on preflight OPTIONS responses."""
1977+
if req.method == "OPTIONS":
1978+
resp.set_header("Access-Control-Max-Age", self._max_age)
1979+
1980+
19611981
class _CapabilitiesMiddleware:
19621982
"""Falcon middleware that sets capability headers on every response."""
19631983

@@ -1988,6 +2008,7 @@ def make_wsgi_app(
19882008
max_request_bytes: int | None = None,
19892009
authenticate: Callable[[falcon.Request], AuthContext] | None = None,
19902010
cors_origins: str | Iterable[str] | None = None,
2011+
cors_max_age: int | None = 7200,
19912012
upload_url_provider: UploadUrlProvider | None = None,
19922013
max_upload_bytes: int | None = None,
19932014
otel_config: object | None = None,
@@ -2040,6 +2061,10 @@ def make_wsgi_app(
20402061
disables CORS headers. Uses Falcon's built-in
20412062
``CORSMiddleware`` which also handles preflight OPTIONS
20422063
requests automatically.
2064+
cors_max_age: Value for the ``Access-Control-Max-Age`` header on
2065+
preflight OPTIONS responses, in seconds. ``7200`` (2 hours)
2066+
by default. ``None`` omits the header. Only effective when
2067+
``cors_origins`` is set.
20432068
upload_url_provider: Optional provider for generating pre-signed
20442069
upload URLs. When set, the ``__upload_url__/init`` endpoint
20452070
is enabled and ``VGI-Upload-URL-Support: true`` is advertised
@@ -2169,6 +2194,8 @@ def make_wsgi_app(
21692194
"expose_headers": cors_expose,
21702195
}
21712196
middleware.append(falcon.CORSMiddleware(**cors_kwargs))
2197+
if cors_max_age is not None:
2198+
middleware.append(_CorsMaxAgeMiddleware(cors_max_age))
21722199
if authenticate is not None:
21732200
on_auth_failure: Callable[[str | None, str], None] | None = None
21742201
if otel_config is not None:

0 commit comments

Comments
 (0)