{"record":{"id":"6dcb98626d40ab3e","repo":"zeroclaw-labs/zeroclaw","slug":"linkedin-list-posts-failed","errorCode":null,"errorMessage":"LinkedIn list_posts failed ({}): {}","messagePattern":"LinkedIn list_posts failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/linkedin_client.rs","lineNumber":324,"sourceCode":"        Ok(post_urn)\n    }\n\n    pub async fn list_posts(&self, count: usize) -> anyhow::Result<Vec<PostSummary>> {\n        let creds = self.get_credentials().await?;\n        let author_urn = format!(\"urn:li:person:{}\", creds.person_id);\n        let url = format!(\n            \"{}/rest/posts?author={}&q=author&count={}\",\n            LINKEDIN_API_BASE, author_urn, count\n        );\n\n        let response = self\n            .api_request(Method::GET, &url, &creds.access_token, None)\n            .await?;\n\n        let status = response.status();\n        if !status.is_success() {\n            let body_text = response.text().await.unwrap_or_default();\n            anyhow::bail!(\"LinkedIn list_posts failed ({}): {}\", status, body_text);\n        }\n\n        let json: serde_json::Value = response\n            .json()\n            .await\n            .context(\"Failed to parse list_posts response\")?;\n\n        let elements = json\n            .get(\"elements\")\n            .and_then(|e| e.as_array())\n            .cloned()\n            .unwrap_or_default();\n\n        let posts = elements\n            .iter()\n            .map(|el| PostSummary {\n                id: el\n                    .get(\"id\")","sourceCodeStart":306,"sourceCodeEnd":342,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/linkedin_client.rs#L306-L342","documentation":"Thrown when GET on the posts listing endpoint returns a non-2xx status, with the raw body included. Listing posts requires read access to the member's shares; the common failure modes are scope/permission (403) and expired token (401). On success the same response is parsed as JSON, so error bodies that are HTML (auth walls) also surface through this path's status check first.","triggerScenarios":"Calling the linkedin list_posts action with an expired access token and no refresh token stored; token granted only posting scope; requesting another member's posts; hitting LinkedIn rate limits (429).","commonSituations":"Long-running services holding tokens beyond the 60-day expiry; developer apps not approved for the requested products; mixing tokens between the developer app that created them.","solutions":["Check the status: 401 => refresh/re-auth; 403 => add the required read scope/product to the app and re-consent; 429 => retry later.","Store the refresh token alongside the access token so api_request can auto-refresh.","Re-authorize the member if the app's scopes changed since the grant."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Distinguish permanent (4xx) from transient (429/5xx) failures\nfor attempt in 0..3 {\n    match client.list_posts(&token).await {\n        Ok(posts) => break Ok(posts),\n        Err(e) if e.to_string().contains(\"(429\") || e.to_string().contains(\"(50\") => {\n            tokio::time::sleep(backoff(attempt)).await;\n        }\n        Err(e) => break Err(e),\n    }?\n}","preventionTips":["Schedule listing with modest intervals to stay clear of LinkedIn rate limits.","Refresh tokens on a timer rather than waiting for a 401 mid-call.","Log the response body once per failure class — LinkedIn's JSON pinpoints scope problems."],"tags":["linkedin","http-status","list-posts"],"backgroundTag":"http-error-response","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}