{"record":{"id":"6cb2f19b5899f42e","repo":"zeroclaw-labs/zeroclaw","slug":"linkedin-image-upload-failed-upload-status-b","errorCode":null,"errorMessage":"LinkedIn image upload failed ({upload_status}): {body_text}","messagePattern":"LinkedIn image upload failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/linkedin_client.rs","lineNumber":673,"sourceCode":"        let mut upload_headers = HeaderMap::new();\n        upload_headers.insert(\n            reqwest::header::AUTHORIZATION,\n            HeaderValue::from_str(&format!(\"Bearer {token}\")).expect(\"valid bearer token header\"),\n        );\n\n        let upload_resp = client\n            .put(&upload_url)\n            .headers(upload_headers)\n            .header(\"Content-Type\", \"image/png\")\n            .body(image_bytes.to_vec())\n            .send()\n            .await\n            .context(\"LinkedIn image upload failed\")?;\n\n        let upload_status = upload_resp.status();\n        if !upload_status.is_success() {\n            let body_text = upload_resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"LinkedIn image upload failed ({upload_status}): {body_text}\");\n        }\n\n        Ok(image_urn)\n    }\n\n    /// Create a post with an attached image.\n    pub async fn create_post_with_image(\n        &self,\n        text: &str,\n        visibility: &str,\n        image_urn: &str,\n        scheduled_at: Option<&str>,\n    ) -> anyhow::Result<String> {\n        let creds = self.get_credentials().await?;\n        let author_urn = format!(\"urn:li:person:{}\", creds.person_id);\n\n        let lifecycle = if scheduled_at.is_some() {\n            \"DRAFT\"","sourceCodeStart":655,"sourceCodeEnd":691,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/linkedin_client.rs#L655-L691","documentation":"Raised by upload_image when the second step of LinkedIn's image flow — POSTing the raw image bytes to the uploadUrl returned by registerUpload — receives a non-2xx HTTP status. The message embeds the status code and LinkedIn's response body, which contains the machine-readable error code and detail. The image URN from step one exists, but the post cannot proceed until the byte upload succeeds.","triggerScenarios":"upload_image() runs after a successful registerUpload: an expired or scope-limited access token yields 401/403; a mismatch between the media type declared at registration and the actual bytes yields 400; LinkedIn throttling yields 429; transient 5xx also surfaces here. A long gap between register and upload lets the short-lived uploadUrl expire.","commonSituations":"Scheduled posting jobs whose token was refreshed elsewhere but not in this workspace; images saved as WEBP/HEIC while registered as image/jpeg; files above LinkedIn's size cap; corporate proxies that rewrite the upload body.","solutions":["Read the body_text embedded in the message and look up the LinkedIn error code (e.g. emptyAccessToken, mediaUploadFailed) before changing anything","If 401/403: refresh the token (LinkedInClient::update_env_token) and rerun the full register + upload flow","Verify the file is JPEG/PNG/GIF, under LinkedIn's limit, and that the media type string matches what was passed to registerUpload","On 429/5xx, retry the upload with exponential backoff; the uploadUrl itself can be reused within its lifetime"],"exampleFix":"// before\nlet urn = client.upload_image(&bytes, \"image/png\").await?;\n\n// after: validate payload first, retry only transient statuses\nensure_image_uploadable(&path)?;\nlet urn = match client.upload_image(&bytes, \"image/png\").await {\n    Ok(urn) => urn,\n    Err(e) if is_transient_http(&e.to_string()) => retry_with_backoff(|| client.upload_image(&bytes, \"image/png\")).await?,\n    Err(e) => return Err(e),\n};","handlingStrategy":"retry","validationCode":"fn ensure_image_uploadable(path: &Path) -> anyhow::Result<()> {\n    let meta = std::fs::metadata(path)?;\n    anyhow::ensure!(meta.len() <= 5 * 1024 * 1024, \"image exceeds LinkedIn size cap\");\n    let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(\"\");\n    anyhow::ensure!(matches!(ext, \"jpg\" | \"jpeg\" | \"png\" | \"gif\"), \"unsupported media type: {ext}\");\n    Ok(())\n}","typeGuard":null,"tryCatchPattern":"Match the embedded status text: on '429' or '5xx' retry the same upload with backoff; on '401'/'403' refresh the token and re-register before retrying; on '400' fix media type/size and never blind-retry.","preventionTips":["Keep register and upload in the same task so the short-lived uploadUrl cannot expire between steps","Refresh LINKEDIN_ACCESS_TOKEN on 401 instead of reusing the same image URN","Always log the embedded body_text; LinkedIn's error code names the exact failing sub-step"],"tags":["linkedin","image-upload","http","social-api"],"backgroundTag":"http-error-response","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}