Commit 0b2b271f authored by Dimitrios Gogos's avatar Dimitrios Gogos
Browse files

fix: resolve one correlation id per request and use it everywhere



x-correlator is optional, and when a caller omitted it three separate pieces of
code each noticed the absence and invented their own answer: the service
generated one for the SRM request body, the SRM client sent no header at all,
and x_correlator_header generated a different one for the response. One request
produced two unrelated UUIDs and no downstream header.

The customer was handed an id that existed only in OEG's response header. It
was never sent anywhere, so quoting it to support matched nothing in SRM's
logs, and the id SRM did record was one the customer never saw. The correlation
id looked like a working feature and did nothing.

oeg/architecture.md is explicit that these are one value, and REQ-OEG-13
requires propagating it to downstream HTTP calls -- so this was a bug, not an
open question.

Resolve it once at the edge and read that everywhere: x_correlator_header
caches on request.state, get_caller_context takes the resolved value, and
routers pass caller.x_correlator. EAM already did this, so it was fixed by the
first two changes alone; QoD's get_qod_session had no caller parameter and
gains one.

LocationRetrievalService also sent the raw parameter as the SRM header while
building correlation_id for the body. Through the router those coincide, but a
direct call with None rebuilt the same divergence inside the service, and tests
and the conformance harness take that path.

test_passes_region_and_status_filters asserted x_correlator=None -- it encoded
the bug, so it is updated rather than preserved. The three new flow tests were
verified to fail when the wiring is reverted; a guard that cannot fail is not a
guard.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent ab6a7639
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -75,5 +75,5 @@ async def retrieve_location(
    return await service.retrieve_location(
        request=request,
        app_provider_id=caller.app_provider_id,
        x_correlator=x_correlator,
        x_correlator=caller.x_correlator,
    )
+4 −3
Original line number Diff line number Diff line
@@ -72,7 +72,7 @@ async def create_qod_session(
        request=request,
        tenant_id=caller.tenant_id,
        app_provider_id=caller.app_provider_id,
        x_correlator=x_correlator,
        x_correlator=caller.x_correlator,
    )


@@ -87,11 +87,12 @@ async def create_qod_session(
async def get_qod_session(
    sessionId: str,
    service: QoDService,
    caller: Caller,
    x_correlator: XCorrelatorHeader = None,
) -> Any:
    return await service.get_session(
        session_id=sessionId,
        x_correlator=x_correlator,
        x_correlator=caller.x_correlator,
    )


@@ -112,7 +113,7 @@ async def delete_qod_session(
        session_id=sessionId,
        tenant_id=caller.tenant_id,
        app_provider_id=caller.app_provider_id,
        x_correlator=x_correlator,
        x_correlator=caller.x_correlator,
    )
    return Response(status_code=status.HTTP_204_NO_CONTENT)

+13 −3
Original line number Diff line number Diff line
@@ -32,10 +32,20 @@ _HTTP_EXCEPTION_MAP: dict[int, type[OEGException]] = {


def x_correlator_header(request: Request) -> str:
    """The one correlation value for this request.

    Resolved once and cached on `request.state`: a generated id must be the same in the body
    OEG sends SRM, the header it sends SRM, and the `X-Correlator` it returns, or the caller
    holds an id that appears in no downstream log (oeg/architecture.md REQ-OEG-13).
    """
    cached = getattr(request.state, "x_correlator", None)
    if isinstance(cached, str):
        return cached
    x_correlator = request.headers.get("x-correlator")
    if x_correlator and X_CORRELATOR_PATTERN.match(x_correlator):
    if not (x_correlator and X_CORRELATOR_PATTERN.match(x_correlator)):
        x_correlator = str(uuid.uuid4())
    request.state.x_correlator = x_correlator
    return x_correlator
    return str(uuid.uuid4())


def register_exception_handlers(app: FastAPI) -> None:
+4 −1
Original line number Diff line number Diff line
@@ -50,6 +50,9 @@ class LocationRetrievalService:
                message="The device cannot be identified.",
            )

        # One value for both surfaces. The router always supplies the caller context's
        # correlator, but a direct call with None must not put a generated id in the body and
        # nothing in the header -- that is the divergence this fallback exists to close.
        correlation_id = x_correlator or str(uuid4())
        query = build_location_query(
            request=request,
@@ -61,7 +64,7 @@ class LocationRetrievalService:

        result = await self.srm_client.retrieve_location(
            query=query,
            x_correlator=x_correlator,
            x_correlator=correlation_id,
        )

        try:
+6 −1
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ from open_exposure_gateway.adapters.database.repos.qod_sessions import (
    SqlQodSessionRepository,
)
from open_exposure_gateway.api.camara.common import XCorrelatorHeader
from open_exposure_gateway.api.error_handlers import x_correlator_header
from open_exposure_gateway.application.services.edge_application_management_service import (
    EdgeApplicationManagementService,
)
@@ -57,10 +58,14 @@ class CallerContext:


def get_caller_context(
    request: Request,
    x_correlator: XCorrelatorHeader = None,
) -> CallerContext:
    # `x_correlator` stays declared so FastAPI still validates the header against CAMARA's
    # pattern and documents it; the value comes from x_correlator_header, which generates and
    # caches one when the caller sent none, so every surface reports the same id.
    return CallerContext(
        x_correlator=x_correlator,
        x_correlator=x_correlator_header(request),
        tenant_id="placeholder",  # TODO: extract from JWT
        app_provider_id="placeholder",  # TODO: extract from JWT
    )
Loading