Skip to content

Commit ee1c642

Browse files
Copilotaepfli
andcommitted
Implement selector passing via gRPC metadata header for in-process mode
- Update GrpcWatcher to pass selector via flagd-selector metadata header - Add GrpcMultiCallableArgs.metadata field to support gRPC metadata - Update README to reflect current header-based implementation - Add test to verify selector is passed via metadata, not request body - Aligns with flagd v0.11.0+ selector normalization standard Co-authored-by: aepfli <[email protected]>
1 parent f0394cd commit ee1c642

File tree

5 files changed

+58
-19
lines changed

5 files changed

+58
-19
lines changed

providers/openfeature-provider-flagd/README.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,9 @@ The default options can be defined in the FlagdProvider constructor.
111111
> [!IMPORTANT]
112112
> This section only applies to **in-process** and **file** resolver modes. RPC mode is not affected by selector handling changes.
113113
114-
#### Migration Guidance
114+
#### Current Implementation
115115

116-
As of flagd v0.11.0 and related services, selector normalization has been updated to prefer gRPC metadata headers over request body fields for the `selector` parameter. This change affects how the in-process provider communicates with flagd's sync service.
117-
118-
**Current Behavior (Preferred):**
119-
The Python SDK currently passes the `selector` via the gRPC request body when using in-process mode. While this approach continues to work for backward compatibility, the flagd ecosystem is transitioning to header-based selector passing.
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.
120117

121118
**Configuration Example:**
122119
```python
@@ -126,22 +123,19 @@ from openfeature.contrib.provider.flagd.config import ResolverType
126123

127124
api.set_provider(FlagdProvider(
128125
resolver_type=ResolverType.IN_PROCESS,
129-
selector="my-flag-source", # Currently passed in request body
126+
selector="my-flag-source", # Passed via flagd-selector header
130127
))
131128
```
132129

133-
#### Backward Compatibility
134-
135-
The current implementation maintains backward compatibility by continuing to pass selectors in the request body. flagd services support both approaches:
136-
- **Request body selector** (current implementation) - Supported for backward compatibility
137-
- **Header-based selector** (future preferred) - Will be supported in a future SDK update
130+
The selector is automatically passed via the `flagd-selector` gRPC metadata header, ensuring compatibility with flagd services that implement selector normalization.
138131

139-
#### Future Breaking Change
132+
#### Backward Compatibility
140133

141-
In a future major version of this SDK, the selector handling will be updated to use gRPC metadata headers (`flagd-selector`) instead of the request body field. This aligns with the flagd ecosystem's standardized approach to selector normalization.
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)
142137

143-
**Action Required:**
144-
No immediate action is required. The current implementation will continue to work with both current and future versions of flagd services. Monitor release notes for announcements about the transition to header-based selectors.
138+
This ensures the Python SDK works correctly with both older and newer versions of flagd services.
145139

146140
**Related Resources:**
147141
- 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: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ 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-
Note: Currently passed via request body. Future versions will use
80-
gRPC metadata headers. See README for migration guidance.
79+
Passed via flagd-selector gRPC metadata header.
8180
:param offline_flag_source_path: the path to the flag source file
8281
:param stream_deadline_ms: the maximum time to wait before a request times out
8382
: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: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,13 +205,24 @@ def shutdown(self) -> None:
205205

206206
def _create_request_args(self) -> dict:
207207
request_args = {}
208-
if self.selector is not None:
209-
request_args["selector"] = self.selector
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
210210
if self.provider_id is not None:
211211
request_args["provider_id"] = self.provider_id
212212

213213
return request_args
214214

215+
def _create_metadata(self) -> typing.Optional[typing.List[typing.Tuple[str, str]]]:
216+
"""Create gRPC metadata headers for the request.
217+
218+
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.
220+
"""
221+
if self.selector is None:
222+
return None
223+
224+
return [("flagd-selector", self.selector)]
225+
215226
def _fetch_metadata(self) -> typing.Optional[sync_pb2.GetMetadataResponse]:
216227
if self.config.sync_metadata_disabled:
217228
return None
@@ -233,6 +244,12 @@ def listen(self) -> None: # noqa: C901
233244
call_args: GrpcMultiCallableArgs = {"wait_for_ready": True}
234245
if self.streamline_deadline_seconds > 0:
235246
call_args["timeout"] = self.streamline_deadline_seconds
247+
248+
# Add selector via gRPC metadata header (flagd v0.11.0+ preferred approach)
249+
metadata = self._create_metadata()
250+
if metadata is not None:
251+
call_args["metadata"] = metadata
252+
236253
request_args = self._create_request_args()
237254

238255
while self.active:

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
class GrpcMultiCallableArgs(typing.TypedDict, total=False):
55
timeout: typing.Optional[float]
66
wait_for_ready: typing.Optional[bool]
7+
metadata: typing.Optional[typing.Sequence[typing.Tuple[str, str]]]

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,31 @@ def test_listen_with_sync_metadata_disabled_in_config(self):
133133
self.provider_details.message, "gRPC sync connection established"
134134
)
135135
self.assertEqual(self.context, {})
136+
137+
def test_selector_passed_via_metadata_header(self):
138+
"""Test that selector is passed via gRPC metadata header, not request body"""
139+
self.grpc_watcher.selector = "test-selector"
140+
mock_stream = iter(
141+
[
142+
SyncFlagsResponse(flag_configuration='{"flag_key": "flag_value"}'),
143+
]
144+
)
145+
self.mock_stub.SyncFlags = Mock(return_value=mock_stream)
146+
147+
self.run_listen_and_shutdown_after()
148+
149+
# Verify SyncFlags was called
150+
self.mock_stub.SyncFlags.assert_called_once()
151+
152+
# Get the call arguments
153+
call_args = self.mock_stub.SyncFlags.call_args
154+
155+
# Verify the request doesn't contain selector in body
156+
request = call_args[0][0] # First positional argument is the request
157+
self.assertFalse(hasattr(request, 'selector') and request.selector)
158+
159+
# Verify metadata contains flagd-selector header
160+
kwargs = call_args[1]
161+
self.assertIn('metadata', kwargs)
162+
metadata = kwargs['metadata']
163+
self.assertEqual(metadata, [('flagd-selector', 'test-selector')])

0 commit comments

Comments
 (0)