From 1195de04acf016965c900beeca6323877fc4d928 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 06:11:33 +0000 Subject: [PATCH] fix(flags): retry DNS, TLS, and connection-refused failures on /flags 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 Generated-By: PostHog Desktop Task-Id: 63bc8676-4e47-42d8-8b84-0bf1fdb94a03 --- .changeset/flags-retry-transport-errors.md | 7 ++++ .../java/com/posthog/internal/PostHogApi.kt | 6 ++++ .../com/posthog/internal/PostHogApiTest.kt | 33 ++++++++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 .changeset/flags-retry-transport-errors.md diff --git a/.changeset/flags-retry-transport-errors.md b/.changeset/flags-retry-transport-errors.md new file mode 100644 index 000000000..b639047b4 --- /dev/null +++ b/.changeset/flags-retry-transport-errors.md @@ -0,0 +1,7 @@ +--- +'posthog': patch +'posthog-android': patch +'posthog-server': patch +--- + +Retry feature flag requests that fail with a DNS, TLS, or connection-refused transport error, instead of only timeouts, EOF, and connection resets diff --git a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt index 3394b6f6e..7309c84fc 100644 --- a/posthog/src/main/java/com/posthog/internal/PostHogApi.kt +++ b/posthog/src/main/java/com/posthog/internal/PostHogApi.kt @@ -22,8 +22,11 @@ import okio.BufferedSink import java.io.EOFException import java.io.IOException import java.io.OutputStream +import java.net.ConnectException import java.net.SocketException import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLException /** * The class that calls the PostHog API @@ -306,6 +309,9 @@ public class PostHogApi( is IOException -> error is SocketTimeoutException || error is EOFException || + error is UnknownHostException || + error is SSLException || + error is ConnectException || (error is SocketException && error.message?.contains("reset", ignoreCase = true) == true) else -> false } diff --git a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt index 393889f8e..e9e9011c8 100644 --- a/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt +++ b/posthog/src/test/java/com/posthog/internal/PostHogApiTest.kt @@ -20,11 +20,14 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import java.io.File import java.io.IOException +import java.net.ConnectException import java.net.InetSocketAddress import java.net.Proxy import java.net.SocketException +import java.net.UnknownHostException import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger +import javax.net.ssl.SSLException import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -401,14 +404,16 @@ internal class PostHogApiTest { } } - @Test - fun `flags does not retry connection refused`() { + private fun assertFlagsRetries( + error: IOException, + expectedAttempts: Int, + ) { val attempts = AtomicInteger(0) val client = OkHttpClient.Builder() .addInterceptor { attempts.incrementAndGet() - throw SocketException("Connection refused") + throw error } .build() val http = mockHttp(response = MockResponse().setBody("{}")) @@ -421,13 +426,33 @@ internal class PostHogApiTest { sut.flags("distinctId", anonymousId = "anonId", groups = emptyMap()) } - assertEquals(1, attempts.get()) + assertEquals(expectedAttempts, attempts.get()) assertEquals(0, http.requestCount) } finally { http.shutdown() } } + @Test + fun `flags retries connection refused`() { + assertFlagsRetries(ConnectException("Connection refused"), expectedAttempts = 2) + } + + @Test + fun `flags retries dns resolution failures`() { + assertFlagsRetries(UnknownHostException("app.posthog.com"), expectedAttempts = 2) + } + + @Test + fun `flags retries tls transport failures`() { + assertFlagsRetries(SSLException("handshake aborted"), expectedAttempts = 2) + } + + @Test + fun `flags does not retry non transport IO failures`() { + assertFlagsRetries(IOException("unexpected end of stream"), expectedAttempts = 1) + } + @Test fun `remote config returns successful response`() { val file = File("src/test/resources/json/basic-remote-config.json")