Skip to content

chore!: return null for malformed feature flag payloads - #889

Merged
turnipdabeets merged 1 commit into
chore/v4-remove-deprecatedfrom
chore/v4-malformed-flag-payloads
Oct 26, 2026
Merged

turnipdabeets merged 1 commit into
chore/v4-remove-deprecatedfrom
chore/v4-malformed-flag-payloads

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

Note

Stacked on the 4.0 integration PR #871. Merges into chore/v4-remove-deprecated, not main.

💡 Motivation and Context

Brings Android in line with the sdk-specs payload decoding contract (get-feature-flag-payload, "Malformed serialized payloads return no payload", plus the matching get-feature-flag-result and get-feature-flags-and-payloads requirements). Mirrors PostHog/posthog-ios#903 (iOS 4.0), which noted that Android needed its own change.

Payload Before After
Malformed ({broken, [1, 2), empty or whitespace-only the raw string null, logged with the flag key
Valid JSON ("hello", "", false, 0, objects, arrays) decoded decoded (unchanged)
JSON null the string "null" null (the spec keeps JSON null, and Kotlin has one null)
Only valid in lenient mode (unquoted hello, single quotes, comments) decoded leniently null
Bootstrapped string "123" / "true" stays a String stays a String (unchanged)
  • The flag's value, enabled state, variant and sibling payloads are unaffected by a bad payload.
  • normalizePayloads in PostHogRemoteConfig now decodes with a strict JsonReader (the shared serializer's Gson is lenient) and rejects trailing data. It runs on every /flags response shape (v4 flags, legacy featureFlagPayloads, and the partial errorsWhileComputingFlags merge), so getFeatureFlagResult, getFeatureFlagPayload and the cache all see the same value.
  • Bootstrap: no double decode on Android, unlike iOS before fix: match capture v1 rules for empty names, drop logs, retry identity and typed values #903. Android decodes /flags payloads once when the response arrives and keeps decoded values in memory and in the cache; bootstrap payloads are served as passed. A new test locks this in (it passes before and after this change).

Public API change: no signature changes; behavior change only (a line in .changeset/android-v4-major.md).

Edge case: the cache stores decoded payloads, and on 3.x a malformed payload was cached as its raw string. That entry is indistinguishable from a real string payload, so an app upgrading from 3.x still reads the raw string from the cache until its first /flags response replaces it. Telling them apart would need a cache format marker, which isn't worth it for this case.

Scope:

  • posthog-server is untouched; it gets its own major, and the spec's server local/remote evaluation rules belong there.
  • posthog-flutter's Android plugin passes getFeatureFlagResult(key)?.payload through unchanged, so no code change; with this, Flutter reads null for a malformed payload on both platforms once it adopts Android 4.0 and iOS 4.0.
  • The React Native Android plugin doesn't read flags or payloads from posthog-android.

💚 How did you test it?

  • New tests in PostHogRemoteConfigTest: malformed ({broken, "", " ", unquoted hello, trailing data) payloads return null while the flag, getFeatureFlags() and a sibling payload are preserved, the parse failure is logged with the flag key, and a new instance reading the cache gets no payload either; valid JSON keeps its type ("hello", "", false, 0, null, object, array); bootstrapped "hello", "123", "true" stay strings.
  • Before the fix, the malformed and valid-JSON tests failed ({broken came back as the raw string, JSON null as the string "null"); the bootstrap test passed, confirming there was no double decode.
  • Updated existing tests that encoded the old behavior: theBroken ([1, 2) now expects null, and the quota-limited fixtures send "\"payload1\"" (valid JSON, as the server does) instead of the unquoted payload1.
  • ./gradlew spotlessCheck apiCheck :posthog:test :posthog-android:testReleaseUnitTest pass (1067 core tests; 765 Android tests, 3 skipped). :posthog-server:test also passes.
  • Not run on a device.

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file (adds a line to .changeset/android-v4-major.md)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Claude Code (Claude Opus 5.5) ported posthog-ios#903 against the sdk-specs contract, wrote the tests first and confirmed them red on the base branch.
  • Strict parsing was chosen over just dropping the raw-string fallback: the shared Gson is lenient, so an unquoted word like hello would otherwise decode to "hello", which is the raw string the spec rules out. iOS's JSONSerialization is strict, so this also keeps the two platforms on the same inputs.
  • The iOS bootstrap double-decode fix was checked and isn't needed here.

🤖 Generated with Claude Code

Matches posthog-ios 4.0 and the sdk-specs payload decoding contract.
@turnipdabeets
turnipdabeets force-pushed the chore/v4-malformed-flag-payloads branch from 0f0bfc8 to 2bfd608 Compare October 26, 2026 23:05
@turnipdabeets
turnipdabeets merged commit 4d7c1fd into chore/v4-remove-deprecated Oct 26, 2026
11 checks passed
@turnipdabeets
turnipdabeets deleted the chore/v4-malformed-flag-payloads branch October 26, 2026 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants