Skip to content

fix(flags): retry DNS, TLS, and connection-refused failures on /flags - #828

Draft
posthog[bot] wants to merge 1 commit into
mainfrom
posthog/android-flags-retry-transport-errors
Draft

posthog[bot] wants to merge 1 commit into
mainfrom
posthog/android-flags-retry-transport-errors

Conversation

@posthog

@posthog posthog Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Contract: http-client — "Feature flag evaluation retry policy" (spec).

The spec says a flag evaluation request SHALL retry when execution failed with a transient transport condition, "such as a network error, connection reset/lost, timeout, DNS/socket/TLS transport failure, or equivalent platform error".

isRetryableFlagsError in PostHogApi only classified SocketTimeoutException, EOFException, and SocketExceptions whose message contains "reset" as retryable. UnknownHostException (DNS), SSLException/SSLHandshakeException (TLS), and ConnectException (connection refused) fell through and were never retried — common conditions on mobile networks, where a flag reload then silently falls back to cached/default values for the rest of the session.

The cross-SDK compliance matrix tracks this as a Partial for this SDK:

Spec requires: transport-layer retry classification treats transient network failures (timeouts, connection resets, DNS/TLS transient failures) as retryable.
SDK currently: Core ingestion transport is solid [...] However, the feature-flags request retry classifier (isRetryableFlagsError) only treats SocketTimeoutException, EOFException, and SocketException messages containing "reset" as retryable [...] UnknownHostException (DNS), SSLException/SSLHandshakeException (TLS), and generic ConnectException (connection refused) fall through unretried.
Backwards compatibility: Backward-compatible — widening this IOException branch only grants extra bounded retries to previously-unretried cases; no signature/config change.
Remediation: Broaden isRetryableFlagsError to cover UnknownHostException, SSLException, and ConnectException.

Why this is backwards-compatible: no public API, config, or default changes. The retry budget is unchanged (featureFlagRequestMaxRetries, default one retry; 0 still disables retries entirely) and the backoff schedule is unchanged. The only behavior difference is that three already-failing cases now get the same bounded retry every other transient transport failure already got. Serialization/programming errors and non-502/504 HTTP statuses are still not retried, as the spec requires.

Scope: the classifier lives in the shared posthog core module, so the fix applies to both posthog-android and posthog-server. Nothing else in the matrix was touched.

💚 How did you test it?

  • Replaced the test that asserted the old connection-refused behavior with a shared helper plus four cases: connection refused, DNS failure, and TLS failure each retry (2 attempts); a plain non-transport IOException still does not (1 attempt).
  • ./gradlew :posthog:test --tests "com.posthog.internal.PostHogApiTest" — pass, new cases confirmed present in the report.
  • make testJava (full core JVM suite) — pass.
  • ./gradlew :posthog-server:test — pass.
  • make checkFormat — pass.

Follow-up (not in this PR): other SocketException subclasses (NoRouteToHostException, PortUnreachableException) are still unretried; the explicit-type list was kept narrow deliberately so response-parse IOExceptions stay non-retryable. Worth revisiting if the spec's "equivalent platform error" language is tightened.

📝 Checklist

  • 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

Created with PostHog Desktop

🤖 Generated with Claude Code

The http-client spec requires flag evaluation requests to retry when the
request failed with a transient transport condition, naming DNS, socket,
and TLS transport failures explicitly. `isRetryableFlagsError` only
covered timeouts, EOF, and `SocketException`s whose message contains
"reset", so `UnknownHostException`, `SSLException`, and
`ConnectException` fell through unretried.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 63bc8676-4e47-42d8-8b84-0bf1fdb94a03
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-10-01 06:17:57 UTC
Duration: 118421ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 389ms
Format Validation.Event Has Uuid ✅ 32ms
Format Validation.Event Has Lib Properties ✅ 31ms
Format Validation.Distinct Id Is String ✅ 25ms
Format Validation.Token Is Present ✅ 25ms
Format Validation.Custom Properties Preserved ✅ 26ms
Format Validation.Event Has Timestamp ✅ 27ms
Retry Behavior.Retries On 503 ✅ 7027ms
Retry Behavior.Does Not Retry On 400 ✅ 4026ms
Retry Behavior.Does Not Retry On 401 ✅ 4024ms
Retry Behavior.Respects Retry After Header ✅ 7024ms
Retry Behavior.Implements Backoff ✅ 17036ms
Retry Behavior.Retries On 500 ✅ 7020ms
Retry Behavior.Retries On 502 ✅ 7020ms
Retry Behavior.Retries On 504 ✅ 7020ms
Retry Behavior.Max Retries Respected ✅ 17024ms
Deduplication.Generates Unique Uuids ✅ 36ms
Deduplication.Preserves Uuid On Retry ✅ 7015ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 12029ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7021ms
Deduplication.No Duplicate Events In Batch ✅ 37ms
Deduplication.Different Events Have Different Uuids ✅ 24ms
Compression.Sends Gzip When Enabled ✅ 19ms
Batch Format.Uses Proper Batch Structure ✅ 19ms
Batch Format.Flush With No Events Sends Nothing ✅ 14ms
Batch Format.Multiple Events Batched Together ✅ 35ms
Error Handling.Does Not Retry On 403 ✅ 4020ms
Error Handling.Does Not Retry On 413 ✅ 4022ms
Error Handling.Retries On 408 ✅ 5025ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 37ms
Request Payload.Flags Request Uses V2 Query Param ✅ 35ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 25ms
Request Payload.Flags Request Omits Authorization Header ✅ 27ms
Request Payload.Token In Flags Body Matches Init ✅ 22ms
Request Payload.Groups Round Trip ✅ 27ms
Request Payload.Groups Default To Empty Object ✅ 25ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 21ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 22ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 22ms
Request Lifecycle.No Flags Request On Init Alone ✅ 8ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 23ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 35ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 23ms
Retry Behavior.Retries Flags On 502 ✅ 323ms
Retry Behavior.Retries Flags On 504 ✅ 325ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 23ms

This branch has not been deployed

No deployments
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.

0 participants