Files
hermes-webui/tests/test_options_content_length.py
T
91606a15e0 Release Batch B: OPTIONS preflight framing + session-index bytes-parse (#5828, #5834) (#5838)
Phase 1 low-risk backend batch (on top of Batch A). Both byte-verified vs PR head,
Codex SAFE, 64 own + 749 regression tests pass, coexists with Batch A's #5833.

- #5828 (ai-ag2026): frame CORS OPTIONS preflight with Content-Length: 0 (keep-alive hang fix)
- #5834 (ai-ag2026): parse session _index.json from bytes not decoded str (all 10 sites)

Co-authored-by: t <a@b>
Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
2026-07-09 09:19:14 -07:00

78 lines
3.0 KiB
Python

"""Regression test — the CORS preflight (OPTIONS) 200 must be framed.
`Handler.do_OPTIONS` answered every preflight with `send_response(200)` +
CORS headers + `end_headers()` but no `Content-Length`. The class runs
`protocol_version = "HTTP/1.1"` (keep-alive on), and its own docstring states
"every response must declare framing". An unframed 200 is "read body until the
connection closes" (RFC 7230 §3.3.3), so a keep-alive client issuing a preflight
(browser CORS for an allowlisted cross-origin front-end, curl, a proxy, a
monitor) blocks reading a body that never arrives until the 30s connection
timeout — and can't reuse the connection.
This pins that the preflight now carries `Content-Length: 0`, sent before
`end_headers()` (so it lands among the emitted headers), mirroring the framing
every other bodyless response in the server already sets. The CORS-header policy
itself (which Origins are echoed) is unchanged and covered separately.
"""
class _FakeHeaders(dict):
def get(self, key, default=None):
return dict.get(self, key, default)
class _OptionsHandler:
"""Captures the send_response / send_header / end_headers sequence."""
def __init__(self, headers=None):
self.headers = _FakeHeaders(headers or {})
self.client_address = ("127.0.0.1", 12345)
self.status = None
self.order = []
def send_response(self, status):
self.status = status
self.order.append(("status", status, None))
def send_header(self, key, value):
self.order.append(("hdr", key.lower(), value))
def end_headers(self):
self.order.append(("end", None, None))
def _run_options(headers=None):
import server
handler = _OptionsHandler(headers)
server.Handler.do_OPTIONS(handler)
return handler
def test_options_sets_content_length_zero():
handler = _run_options()
assert handler.status == 200
header_map = {k: v for kind, k, v in handler.order if kind == "hdr"}
assert header_map.get("content-length") == "0", (
"OPTIONS preflight 200 must be framed with Content-Length: 0"
)
def test_options_content_length_precedes_end_headers():
"""Framing must land before end_headers() to be emitted at all."""
handler = _run_options()
end_idx = next(i for i, e in enumerate(handler.order) if e[0] == "end")
header_names = [k for kind, k, _ in handler.order[:end_idx] if kind == "hdr"]
assert "content-length" in header_names
def test_options_framed_even_for_allowlisted_origin(monkeypatch):
"""The framing is unconditional — a preflight that DOES emit CORS headers
(allowlisted origin) must still be framed, not just the no-origin case."""
import api.routes as routes
monkeypatch.setattr(
routes, "_check_same_origin_browser_request", lambda handler: True
)
handler = _run_options({"Origin": "https://app.example"})
header_map = {k: v for kind, k, v in handler.order if kind == "hdr"}
assert header_map.get("content-length") == "0"