From 71103fc09728bffc20786b5f66088d8feb89f103 Mon Sep 17 00:00:00 2001 From: SavinduDimal Date: Mon, 13 Jul 2026 14:10:52 +0530 Subject: [PATCH 1/2] Add SDK changes to clone header context --- sdk/core/policy/v1alpha2/context.go | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/sdk/core/policy/v1alpha2/context.go b/sdk/core/policy/v1alpha2/context.go index 8cd784e94..434d0f7b4 100644 --- a/sdk/core/policy/v1alpha2/context.go +++ b/sdk/core/policy/v1alpha2/context.go @@ -17,6 +17,29 @@ type Body struct { Present bool } +// DownstreamContext holds an immutable snapshot of data as received from the +// downstream client, captured before any policy mutation is applied. +// +// Downstream is nil on older gateways that predate this field. Policies MUST +// nil-check before use and fall back to legacy validation. +// +type DownstreamContext struct { + // Headers is the original request headers exactly as received from the + // downstream client, before any policy mutation. + Headers *Headers +} + +// UpstreamContext holds an immutable snapshot of data as received from the +// upstream backend, captured before any policy mutation is applied. +// +// Upstream is nil on older gateways that predate this field. Policies MUST +// nil-check before use and fall back to legacy validation. +type UpstreamContext struct { + // Headers is the original response headers exactly as received from the + // upstream backend, before any policy mutation. Read-only. + Headers *Headers +} + // SharedContext contains data shared across request and response phases type SharedContext struct { // ProjectID is the project ID which the API is associated with @@ -75,6 +98,10 @@ type RequestHeaderContext struct { Authority string Scheme string Vhost string + + // Downstream holds the immutable snapshot of the original client request + // headers, captured before any policy mutation. Nil on older gateways. + Downstream *DownstreamContext } // RequestContext is passed to RequestPolicy.OnRequestBody. @@ -99,6 +126,10 @@ type RequestContext struct { // used to address the actual upstream (e.g. for request signing) — use // UpstreamInfo.URL instead. UpstreamInfo *policyenginev1.UpstreamInfo + + // Downstream holds the immutable snapshot of the original client request + // headers, captured before any policy mutation. Nil on older gateways. + Downstream *DownstreamContext } // ─── Response-phase contexts ───────────────────────────────────────────────── @@ -119,6 +150,14 @@ type ResponseHeaderContext struct { // Current response status code ResponseStatus int + + // Downstream holds the immutable snapshot of the original client request + // headers, captured before any policy mutation. Nil on older gateways. + Downstream *DownstreamContext + + // Upstream holds the immutable snapshot of the original upstream response + // headers, captured before any policy mutation. Nil on older gateways. + Upstream *UpstreamContext } // ResponseContext is passed to ResponsePolicy.OnResponseBody. @@ -143,6 +182,14 @@ type ResponseContext struct { // Current response status code ResponseStatus int + + // Downstream holds the immutable snapshot of the original client request + // headers, captured before any policy mutation. Nil on older gateways. + Downstream *DownstreamContext + + // Upstream holds the immutable snapshot of the original upstream response + // headers, captured before any policy mutation. Nil on older gateways. + Upstream *UpstreamContext } // ─── Streaming contexts ────────────────────────────────────────────────────── @@ -180,6 +227,10 @@ type RequestStreamContext struct { Authority string Scheme string Vhost string + + // Downstream holds the immutable snapshot of the original client request + // headers, captured before any policy mutation. Nil on older gateways. + Downstream *DownstreamContext } // ResponseStreamContext is the per-chunk context passed to StreamingResponseBodyPolicy. @@ -199,4 +250,12 @@ type ResponseStreamContext struct { // Current response status code ResponseStatus int + + // Downstream holds the immutable snapshot of the original client request + // headers, captured before any policy mutation. Nil on older gateways. + Downstream *DownstreamContext + + // Upstream holds the immutable snapshot of the original upstream response + // headers, captured before any policy mutation. Nil on older gateways. + Upstream *UpstreamContext } From cab5934933d71d9f912fe22ab0c823a27a289b39 Mon Sep 17 00:00:00 2001 From: SavinduDimal Date: Mon, 13 Jul 2026 14:18:27 +0530 Subject: [PATCH 2/2] Add python SDK changes to clone header context --- .../apip_sdk_core/policy/v1alpha2/__init__.py | 4 ++ .../apip_sdk_core/policy/v1alpha2/types.py | 40 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/sdk-python/src/apip_sdk_core/policy/v1alpha2/__init__.py b/sdk-python/src/apip_sdk_core/policy/v1alpha2/__init__.py index 0f2050af4..7128d3df7 100644 --- a/sdk-python/src/apip_sdk_core/policy/v1alpha2/__init__.py +++ b/sdk-python/src/apip_sdk_core/policy/v1alpha2/__init__.py @@ -44,6 +44,7 @@ AuthContext, Body, BodyProcessingMode, + DownstreamContext, ExecutionContext, ExecutionPhase, HeaderProcessingMode, @@ -58,12 +59,14 @@ ResponseStreamContext, SharedContext, StreamBody, + UpstreamContext, ) __all__ = [ "AuthContext", "Body", "BodyProcessingMode", + "DownstreamContext", "DownstreamResponseHeaderModifications", "DownstreamResponseModifications", "DropHeaderAction", @@ -98,6 +101,7 @@ "StreamingResponseAction", "StreamingResponsePolicy", "TerminateResponseChunk", + "UpstreamContext", "UpstreamRequestHeaderModifications", "UpstreamRequestModifications", ] diff --git a/sdk-python/src/apip_sdk_core/policy/v1alpha2/types.py b/sdk-python/src/apip_sdk_core/policy/v1alpha2/types.py index a6f89a3b2..536361390 100644 --- a/sdk-python/src/apip_sdk_core/policy/v1alpha2/types.py +++ b/sdk-python/src/apip_sdk_core/policy/v1alpha2/types.py @@ -129,6 +129,37 @@ class StreamBody: index: int = 0 +@dataclass(slots=True) +class DownstreamContext: + """Immutable snapshot of the original client request headers, captured + before any policy mutation. + + ``None`` on older gateways that predate this field. Policies MUST treat a + ``None`` ``downstream`` (or a ``None`` ``headers``) as "not available" and + fall back to legacy validation against the mutable headers. + + ``headers`` is ``Headers | None`` (defaulting to ``None``) to mirror the Go + SDK's nilable ``Headers *Headers``: the kernel leaves it ``None`` when no + snapshot is available, rather than substituting an empty ``Headers()`` that + a policy could not distinguish from "the client sent no headers". + """ + + headers: Headers | None = None + + +@dataclass(slots=True) +class UpstreamContext: + """Immutable snapshot of the original upstream response headers, captured + before any policy mutation. ``None`` on older gateways; see + :class:`DownstreamContext` for the backward-compat contract. + + ``headers`` is ``Headers | None`` (defaulting to ``None``), mirroring the Go + SDK's nilable ``Headers *Headers`` — see :class:`DownstreamContext`. + """ + + headers: Headers | None = None + + @dataclass(slots=True) class RequestHeaderContext: shared: SharedContext @@ -138,6 +169,7 @@ class RequestHeaderContext: authority: str = "" scheme: str = "" vhost: str = "" + downstream: DownstreamContext | None = None @dataclass(slots=True) @@ -150,6 +182,7 @@ class RequestContext: authority: str = "" scheme: str = "" vhost: str = "" + downstream: DownstreamContext | None = None @dataclass(slots=True) @@ -161,6 +194,8 @@ class ResponseHeaderContext: request_method: str = "" response_headers: Headers = field(default_factory=Headers) response_status: int = 200 + downstream: DownstreamContext | None = None + upstream: UpstreamContext | None = None @dataclass(slots=True) @@ -173,6 +208,8 @@ class ResponseContext: response_headers: Headers = field(default_factory=Headers) response_body: Body | None = None response_status: int = 200 + downstream: DownstreamContext | None = None + upstream: UpstreamContext | None = None @dataclass(slots=True) @@ -184,6 +221,7 @@ class RequestStreamContext: authority: str = "" scheme: str = "" vhost: str = "" + downstream: DownstreamContext | None = None @dataclass(slots=True) @@ -195,6 +233,8 @@ class ResponseStreamContext: request_method: str = "" response_headers: Headers = field(default_factory=Headers) response_status: int = 200 + downstream: DownstreamContext | None = None + upstream: UpstreamContext | None = None @dataclass(slots=True)