Skip to content

Commit b66ff85

Browse files
Copilotaepfli
andcommitted
Pass selector via both header and request body for backward compatibility
- Update _create_request_args to include selector in request body - Maintain selector in flagd-selector gRPC metadata header - Update test to verify both header and body contain selector - Update documentation to reflect dual transmission approach - Ensures compatibility with both older and newer flagd versions Co-authored-by: aepfli <[email protected]>
1 parent ee1c642 commit b66ff85

File tree

4 files changed

+22
-17
lines changed

4 files changed

+22
-17
lines changed

providers/openfeature-provider-flagd/README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ The default options can be defined in the FlagdProvider constructor.
113113
114114
#### Current Implementation
115115

116-
As of this SDK version, the `selector` parameter is passed via gRPC metadata headers (`flagd-selector`) when using in-process mode. This aligns with flagd v0.11.0+ selector normalization standards.
116+
As of this SDK version, the `selector` parameter is passed via **both** gRPC metadata headers (`flagd-selector`) and the request body when using in-process mode. This dual approach ensures maximum compatibility with all flagd versions.
117117

118118
**Configuration Example:**
119119
```python
@@ -123,19 +123,20 @@ from openfeature.contrib.provider.flagd.config import ResolverType
123123

124124
api.set_provider(FlagdProvider(
125125
resolver_type=ResolverType.IN_PROCESS,
126-
selector="my-flag-source", # Passed via flagd-selector header
126+
selector="my-flag-source", # Passed via both header and request body
127127
))
128128
```
129129

130-
The selector is automatically passed via the `flagd-selector` gRPC metadata header, ensuring compatibility with flagd services that implement selector normalization.
130+
The selector is automatically passed via:
131+
- **gRPC metadata header** (`flagd-selector`) - For flagd v0.11.0+ selector normalization
132+
- **Request body** - For backward compatibility with older flagd versions
131133

132134
#### Backward Compatibility
133135

134-
flagd services maintain backward compatibility with both selector passing approaches:
135-
- **gRPC metadata header** (`flagd-selector`) - Current implementation (recommended)
136-
- **Request body selector** - Legacy approach (still supported by flagd for backward compatibility)
137-
138-
This ensures the Python SDK works correctly with both older and newer versions of flagd services.
136+
This dual transmission approach ensures the Python SDK works seamlessly with all flagd service versions:
137+
- **Older flagd versions** read the selector from the request body
138+
- **Newer flagd versions (v0.11.0+)** prefer the selector from the gRPC metadata header
139+
- Both approaches are supported simultaneously for maximum compatibility
139140

140141
**Related Resources:**
141142
- Upstream issue: [open-feature/flagd#1814](https://github.com/open-feature/flagd/issues/1814)

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/provider.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ def __init__( # noqa: PLR0913
7676
:param timeout: the maximum time to wait before a request times out
7777
:param retry_backoff_ms: the number of milliseconds to backoff
7878
:param selector: filter flag configurations by source (in-process mode only)
79-
Passed via flagd-selector gRPC metadata header.
79+
Passed via both flagd-selector gRPC metadata header and request body
80+
for backward compatibility with all flagd versions.
8081
:param offline_flag_source_path: the path to the flag source file
8182
:param stream_deadline_ms: the maximum time to wait before a request times out
8283
:param keep_alive_time: the number of milliseconds to keep alive

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,10 @@ def shutdown(self) -> None:
205205

206206
def _create_request_args(self) -> dict:
207207
request_args = {}
208-
# Note: selector is now passed via gRPC metadata header instead of request body
209-
# This maintains compatibility with flagd v0.11.0+ selector normalization
208+
# Pass selector in both request body (legacy) and metadata header (new) for backward compatibility
209+
# This ensures compatibility with both older and newer flagd versions
210+
if self.selector is not None:
211+
request_args["selector"] = self.selector
210212
if self.provider_id is not None:
211213
request_args["provider_id"] = self.provider_id
212214

@@ -216,7 +218,8 @@ def _create_metadata(self) -> typing.Optional[typing.List[typing.Tuple[str, str]
216218
"""Create gRPC metadata headers for the request.
217219
218220
Returns gRPC metadata as a list of tuples containing header key-value pairs.
219-
The selector is passed via the 'flagd-selector' header per flagd v0.11.0+ specification.
221+
The selector is passed via the 'flagd-selector' header per flagd v0.11.0+ specification,
222+
while also being included in the request body for backward compatibility with older flagd versions.
220223
"""
221224
if self.selector is None:
222225
return None

providers/openfeature-provider-flagd/tests/test_grpc_watcher.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ def test_listen_with_sync_metadata_disabled_in_config(self):
134134
)
135135
self.assertEqual(self.context, {})
136136

137-
def test_selector_passed_via_metadata_header(self):
138-
"""Test that selector is passed via gRPC metadata header, not request body"""
137+
def test_selector_passed_via_both_metadata_and_body(self):
138+
"""Test that selector is passed via both gRPC metadata header and request body for backward compatibility"""
139139
self.grpc_watcher.selector = "test-selector"
140140
mock_stream = iter(
141141
[
@@ -152,11 +152,11 @@ def test_selector_passed_via_metadata_header(self):
152152
# Get the call arguments
153153
call_args = self.mock_stub.SyncFlags.call_args
154154

155-
# Verify the request doesn't contain selector in body
155+
# Verify the request contains selector in body (backward compatibility)
156156
request = call_args[0][0] # First positional argument is the request
157-
self.assertFalse(hasattr(request, 'selector') and request.selector)
157+
self.assertEqual(request.selector, 'test-selector')
158158

159-
# Verify metadata contains flagd-selector header
159+
# Verify metadata also contains flagd-selector header (new approach)
160160
kwargs = call_args[1]
161161
self.assertIn('metadata', kwargs)
162162
metadata = kwargs['metadata']

0 commit comments

Comments
 (0)