v0.35.0 — CORS pinned cross-language
CORS is now part of the cross-language contract
The conformance suite had no CORS coverage — only Python-local tests, and those asserted four standard expose headers rather than the capability ones.
That gap mattered more than its size suggests. Every other test in the suite drives the server with an ordinary HTTP client, which sees all response headers and may send any request header. A browser does neither. So a port could implement VGI-Auth-Reason, VGI-Sticky-Enabled, VGI-Max-Response-Bytes and X-VGI-RPC-Error perfectly, pass every group, and ship a server from which a browser client could read not one capability — with nothing in the suite saying so.
TestCors pins both halves, because they fail differently.
Response half — Access-Control-Expose-Headers
A browser hides every response header not on this list from JavaScript. That covers the whole capability system, the session headers, VGI-Auth-Reason on a 401, and X-VGI-RPC-Error — which is how a client tells an error response from a result, so without it a browser client cannot distinguish the two at all.
The rule is just: whatever you advertise, expose. test_advertised_capabilities_are_all_exposed derives its expectation from what your server actually advertises on OPTIONS /health, so it adapts to your feature set instead of demanding features you never claimed.
Request half — Access-Control-Allow-Headers
A browser refuses to send a header the preflight did not permit. Falcon echoes Access-Control-Request-Headers back, so the Python reference is permissive by construction — but a port that hardcodes Content-Type instead looks completely healthy on ordinary calls and then silently breaks sticky sessions (VGI-Session, VGI-Session-Accept), proxy proof (VGI-Proxy-Proof), and encoding negotiation (X-VGI-Accept-Encoding). Echoing the request, an explicit list, or * are all conformant.
Two more things the tests pin
application/vnd.apache.arrow.streamis not a CORS-safelistedContent-Type, so every RPC call is a preflighted request. There is no simple-request fast path.Access-Control-Allow-Originmust be on the actual response, not only the preflight — a browser re-checks it and discards the body without it. A server that sets it onOPTIONSalone fails every real call while passing a naive preflight-only test.
For porters
Supply an optional conformance_http_cors_port fixture — a worker allowing the origin https://conformance.example — and the group runs. Omit it and TestCors skips, which leaves the gap open rather than closing it. TestCorsOffMode is ungated and runs everywhere: a server with no CORS configured MUST emit no CORS headers.
See the CORS section of docs/porting-guide.md.
serve_http can now enable CORS
serve_http(
server,
+ cors_origins="https://app.example",
+ cors_max_age=7200,
)serve_http forwarded twelve other make_wsgi_app keyword arguments but not these two, so serving a CORS-enabled worker meant dropping down to make_wsgi_app and bringing your own WSGI server. An omission, not a decision.
Upgrading: no action required. Both parameters default to the existing behaviour (cors_origins=None — no CORS headers at all), and nothing about the wire contract changes.