Repository navigation
feat!: support a separate streaming host and custom request headers - #175
Open
vazarkevych wants to merge 3 commits into
Open
vazarkevych wants to merge 3 commits into
vazarkevych wants to merge 3 commits into
Conversation
Endpoints were built two ways: GBContext assembled the features and remote-eval URLs itself, while FeatureURLBuilder assembled the ones the data source actually requested, including streaming. Both were correct but had to be kept in step by hand, and a second host for streaming would have had to be threaded through both. FeatureURLBuilder is now an instance holding the hosts, GBContext delegates to it and gained getStreamingURL(), and the data source asks the context instead of rebuilding URLs. Behaviour is unchanged: a path on the host is still preserved and trailing slashes are still dropped.
The SDK talked to a single host with only the headers it sets itself, so it could not be pointed at a deployment where streaming is served separately - a GrowthBook Proxy in front of a CDN - nor used behind a gateway that expects an authentication header. Both are configurable in the reference JS SDK. Adds streamingHost, which the streaming endpoint uses and which falls back to hostURL, plus apiHostRequestHeaders (features request and remote evaluation) and streamingHostRequestHeaders (streaming connection and its reconnects). The maps are copied on the way in so a later edit by the caller cannot change what requests are already carrying. Headers the SDK manages - User-Agent, If-None-Match, Cache-Control - are rejected at configuration time with an ArgumentError, along with a streamingHost that is not an absolute http/https URL, so a misconfiguration surfaces at startup instead of as a failed fetch. This is stricter than the JS SDK, which merges caller headers over its own and would let a supplied If-None-Match silently break ETag revalidation. BaseClient gains an optional headers argument on its three methods, which is a breaking change for anyone supplying a custom client.
andywhite37
self-requested a review
October 8, 2026 21:30
Contributor
|
Please confirm the streaming-host/header API, reserved-header policy, and release target for the breaking BaseClient signatures. Existing custom clients must update their overrides, and #176 adds clients needing the same adjustment. Record the compatibility decision so review can proceed. |
Open
Collaborator
Author
|
Decisions:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The SDK talked to a single host with only the headers it sets itself, so it could not be pointed at a
deployment where streaming is served separately — features from a CDN, streaming through a GrowthBook
Proxy — nor used behind a gateway that expects an authentication header. Both are configurable in the
reference JS SDK; this brings the Flutter SDK to parity.
streamingHosthostURLapiHostRequestHeadersstreamingHostRequestHeadersBehaviour
Semantics follow
getApiHosts()in sdk-js:the streaming host falls back to the API host, trailing slashes are dropped, and a path on the host is
preserved. Features and remote evaluation always stay on
hostURL.The header maps are copied on the way in, so a later edit of the caller's map cannot change what
requests are already carrying. Streaming headers are re-applied on every SSE reconnect, not just on
the first connection.
Reserved headers are rejected, unlike in the JS SDK. JS merges caller headers over its own
(
{ "Content-Type": ..., ...headers }), which lets a suppliedIf-None-Matchsilently break ETagrevalidation. Here
User-Agent,If-None-MatchandCache-Controlthrow anArgumentErrorfrominitialize()in any letter case, matching the C# SDK, and the client still applies its own headerslast as a second line of defence.
Content-Typeis deliberately not reserved — settingtext/event-streamon the streaming connection is a real nginx workaround.A
streamingHostthat is not an absolutehttp/httpsURL is rejected the same way, so amisconfiguration surfaces at startup rather than as a failed fetch later.
Refactor in the first commit
Endpoints were built two ways:
GBContextassembled the features and remote-eval URLs itself, whileFeatureURLBuilderassembled the ones the data source actually requested, including streaming. Bothwere correct but had to be kept in step by hand, and a second host would have had to be threaded
through both.
FeatureURLBuilderis now an instance holding the hosts,GBContextdelegates to itand gained
getStreamingURL(), and the data source asks the context. No behaviour change.Breaking change
BaseClientgained an optional{Map<String, String>? headers}on its three methods — the onlychannel through which headers can reach an injected client. This breaks anyone supplying a custom
client via
client:; inside the repo it only affected the test doubles. Needs a CHANGELOG note.Tests
+19 tests, 321 in the suite, all green.
flutter analyzeclean,dart formatclean.gb_sdk_request_headers_test.dart(14): API headers on the features GET and on the remote-evalPOST but not on the stream; streaming headers on the stream but not on the API host; nothing added
when unconfigured; a later edit of the caller's map does not leak through; SSE uses
streamingHostwhen set and
hostURLwhen not, while features and remote eval stay on the API host; invalidstreaming hosts and reserved header names (any case, both maps) throw
ArgumentError.utility_classes_test.dart(+5, plus the two existing builder tests updated): features, streamingand remote-eval endpoints, host path preservation, null host or client key, and the streaming-host
fallback.
Notes
tests cover the failure path.
apiHostis not added as a separate option — the existinghostURLalready plays that role andrenaming it would be breaking for no gain.
feature/attributes-merge-semanticsbranch: it adds three test clientsthat implement
BaseClient. Whichever of the two merges second needs the newheadersparameteradded to those overrides — about five lines, and CI flags it immediately.