Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion horizon/facts/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from horizon.startup.api_keys import get_env_api_key
from horizon.startup.remote_config import get_remote_config

CONSISTENT_UPDATE_HEADER = "X-Permit-Consistent-Update"


class FactsClient:
def __init__(self):
Expand All @@ -38,16 +40,21 @@ async def build_forward_request(
path: str,
*,
query_params: dict[str, Any] | None = None,
is_consistent_update: bool = False,
) -> HttpxRequest:
"""
Build an HTTPX request from a FastAPI request to forward to the facts service.
:param request: FastAPI request
:param path: Backend facts service path to forward to
:param is_consistent_update: if True, marks the request as a consistent update so the
backend skips the control-plane delta update (the PDP handles propagation locally).
:return: HTTPX request
"""
forward_headers = {
key: value for key, value in request.headers.items() if key.lower() in {"authorization", "content-type"}
}
if is_consistent_update:
forward_headers[CONSISTENT_UPDATE_HEADER] = "true"
Comment thread
omer9564 marked this conversation as resolved.
Outdated
remote_config = get_remote_config()
project_id = remote_config.context.get("project_id")
environment_id = remote_config.context.get("env_id")
Expand Down Expand Up @@ -77,14 +84,18 @@ async def send_forward_request(
path: str,
*,
query_params: dict[str, Any] | None = None,
is_consistent_update: bool = False,
) -> HttpxResponse:
"""
Send a forward request to the facts service.
:param request: FastAPI request
:param path: Backend facts service path to forward to
:param is_consistent_update: see build_forward_request.
:return: HTTPX response
"""
forward_request = await self.build_forward_request(request, path, query_params=query_params)
forward_request = await self.build_forward_request(
request, path, query_params=query_params, is_consistent_update=is_consistent_update
)
Comment thread
omer9564 marked this conversation as resolved.
return await self.send(forward_request)

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion horizon/facts/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ async def forward_request_then_wait_for_update(
query_params: dict[str, Any] | None = None,
) -> Response:
_update_id = update_id or uuid4()
response = await client.send_forward_request(request, path, query_params=query_params)
response = await client.send_forward_request(request, path, query_params=query_params, is_consistent_update=True)
Comment thread
omer9564 marked this conversation as resolved.
Comment thread
omer9564 marked this conversation as resolved.
body = client.extract_body(response)
if body is None:
return client.convert_response(response)
Expand Down
61 changes: 61 additions & 0 deletions horizon/tests/test_facts_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest.mock import MagicMock, patch

import pytest
from horizon.facts.client import CONSISTENT_UPDATE_HEADER, FactsClient
from starlette.requests import Request as FastApiRequest


def _make_request(headers: dict[str, str] | None = None) -> FastApiRequest:
scope = {
"type": "http",
"method": "POST",
"path": "/facts/users",
"raw_path": b"/facts/users",
"query_string": b"",
"headers": [(k.lower().encode(), v.encode()) for k, v in (headers or {}).items()],
}

async def receive():
return {"type": "http.request", "body": b"", "more_body": False}

return FastApiRequest(scope, receive)


def test_consistent_update_header_constant():
assert CONSISTENT_UPDATE_HEADER == "X-Permit-Consistent-Update"
Comment thread
omer9564 marked this conversation as resolved.
Outdated


@pytest.mark.asyncio
async def test_build_forward_request_adds_header_when_consistent_update():
"""When is_consistent_update=True, request should carry the X-Permit-Consistent-Update header."""
client = FactsClient()

mock_remote_config = MagicMock()
mock_remote_config.context = {"project_id": "proj1", "env_id": "env1"}

with (
patch("horizon.facts.client.get_remote_config", return_value=mock_remote_config),
patch("horizon.facts.client.get_env_api_key", return_value="test_api_key"),
):
request = _make_request(headers={"authorization": "Bearer user_token", "content-type": "application/json"})
forward_request = await client.build_forward_request(request, "/users", is_consistent_update=True)

assert forward_request.headers.get(CONSISTENT_UPDATE_HEADER) == "true"


@pytest.mark.asyncio
async def test_build_forward_request_omits_header_by_default():
"""By default (fallback proxy path), the request should NOT carry the consistent-update header."""
client = FactsClient()

mock_remote_config = MagicMock()
mock_remote_config.context = {"project_id": "proj1", "env_id": "env1"}

with (
patch("horizon.facts.client.get_remote_config", return_value=mock_remote_config),
patch("horizon.facts.client.get_env_api_key", return_value="test_api_key"),
):
request = _make_request(headers={"authorization": "Bearer user_token", "content-type": "application/json"})
forward_request = await client.build_forward_request(request, "/anything")

assert forward_request.headers.get(CONSISTENT_UPDATE_HEADER) is None
Loading