{"record":{"id":"414ba0adfd8a845d","repo":"neondatabase/neon","slug":"failed-to-get-feature-flags","errorCode":null,"errorMessage":"Failed to get feature flags: {}, {}","messagePattern":"Failed to get feature flags: (.+?), (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"libs/posthog_client_lite/src/lib.rs","lineNumber":577,"sourceCode":"            )\n        } else {\n            // The old personal API token\n            format!(\n                \"{}/api/projects/{}/feature_flags/local_evaluation\",\n                self.config.private_api_url, self.config.project_id\n            )\n        };\n        let response = self\n            .client\n            .get(url)\n            .bearer_auth(&self.config.server_api_key)\n            .send()\n            .await?;\n        let status = response.status();\n        let body = response.text().await?;\n        if !status.is_success() {\n            return Err(anyhow::anyhow!(\n                \"Failed to get feature flags: {}, {}\",\n                status,\n                body\n            ));\n        }\n        Ok(body)\n    }\n\n    /// Fetch the feature flag specs from the server.\n    ///\n    /// This is unfortunately an undocumented API at:\n    /// - <https://posthog.com/docs/api/feature-flags#get-api-projects-project_id-feature_flags-local_evaluation>\n    /// - <https://posthog.com/docs/feature-flags/local-evaluation>\n    ///\n    /// The handling logic in [`FeatureStore`] mostly follows the Python API implementation.\n    /// See `_compute_flag_locally` in <https://github.com/PostHog/posthog-python/blob/master/posthog/client.py>\n    pub async fn get_feature_flags_local_evaluation(\n        &self,\n    ) -> Result<LocalEvaluationResponse, anyhow::Error> {","sourceCodeStart":559,"sourceCodeEnd":595,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/posthog_client_lite/src/lib.rs#L559-L595","documentation":"get_feature_flags_local_evaluation_raw issues a GET with the server_api_key as a bearer token to either /api/feature_flag/local_evaluation (new secure key) or /api/projects/{project_id}/feature_flags/local_evaluation (legacy personal token). If PostHog answers with a non-2xx status, the error embeds both the HTTP status and the response body, which usually states the exact reason (invalid key, unknown project, plan limits).","triggerScenarios":"401/403 with a wrong or expired server_api_key; 404 from a wrong project_id or wrong private_api_url base; 402 or plan-related errors when the PostHog plan lacks local evaluation; 5xx during a PostHog outage -- each surfaces as 'Failed to get feature flags: <status>, <body>'.","commonSituations":"Rotated personal API keys not updated in config; mixing up client_api_key (public) and server_api_key (private); pointing private_api_url at the public capture endpoint; self-hosted PostHog behind an auth-stripping proxy; egress blocked by firewall producing proxied 4xx/5xx responses.","solutions":["Read the embedded response body first -- it distinguishes bad key (401), bad project (404), and plan/limit errors","Verify server_api_key is a PostHog personal/secure API token with read access to the project, and project_id matches it","Confirm private_api_url is the private/API base URL of your PostHog instance, not the public capture URL","For 5xx/timeouts, retry with backoff (transient PostHog or network issues); for 4xx fix credentials/configuration instead"],"exampleFix":"# before\nPOSTHOG_API_URL='https://eu.i.posthog.com'        # public capture URL used as private API\nPOSTHOG_API_KEY='phx_...'                          # client key used as server key\n\n# after\nPOSTHOG_API_URL='https://eu.i.posthog.com'          # same host is fine for cloud\nPOSTHOG_SERVER_API_KEY='phx_team_personal_token'    # personal/secure token w/ read scope","handlingStrategy":"retry","validationCode":"// Cheap pre-flight: distinguish credential errors (do not retry) from transient ones.\nasync fn fetch_feature_flags_guarded(client: &PosthogClientLite) -> anyhow::Result<String> {\n    match client.get_feature_flags_local_evaluation_raw().await {\n        Err(e) => {\n            let msg = format!(\"{e:#}\");\n            if msg.contains(\" 401, \") || msg.contains(\" 403, \") || msg.contains(\" 404, \") {\n                anyhow::bail!(\"posthog credentials/project misconfigured (fatal): {msg}\");\n            }\n            Err(e) // 5xx / timeouts: retriable by caller\n        }\n        ok => ok,\n    }\n}","typeGuard":null,"tryCatchPattern":"use backoff::{backoff::Backoff, ExponentialBackoff};\n\nlet mut bo = ExponentialBackoff::default(); // default max elapsed ~15m, jittered\nloop {\n    match client.get_feature_flags_local_evaluation_raw().await {\n        Ok(body) => break Ok(body),\n        Err(e) => {\n            let msg = format!(\"{e:#}\");\n            let fatal = msg.contains(\" 401, \") || msg.contains(\" 403, \") || msg.contains(\" 404, \");\n            if fatal || bo.next_backoff().is_none() {\n                break Err(anyhow::anyhow!(\"feature flag fetch failed: {msg}\"));\n            }\n            tracing::warn!(\"retrying posthog feature flags after: {msg}\");\n        }\n    }\n}","preventionTips":["Separate public (client_api_key) and private (server_api_key) credentials in config from day one","Run a startup probe: one feature-flag GET, and fail fast on 401/404 with the body logged","Cache last-known-good flags so transient PostHog failures degrade instead of erroring","Monitor the embedded status/body pairs; a sudden 401 after working usually means key rotation"],"tags":["rust","posthog","feature-flags","api","authentication"],"backgroundTag":"http-api-request-failed","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}