zeroclaw-labs/zeroclaw · error
LinkedIn image upload failed ({upload_status}): {body_text}
Error message
LinkedIn image upload failed ({upload_status}): {body_text} What it means
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.
Source
Thrown at crates/zeroclaw-tools/src/linkedin_client.rs:673
let mut upload_headers = HeaderMap::new();
upload_headers.insert(
reqwest::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}")).expect("valid bearer token header"),
);
let upload_resp = client
.put(&upload_url)
.headers(upload_headers)
.header("Content-Type", "image/png")
.body(image_bytes.to_vec())
.send()
.await
.context("LinkedIn image upload failed")?;
let upload_status = upload_resp.status();
if !upload_status.is_success() {
let body_text = upload_resp.text().await.unwrap_or_default();
anyhow::bail!("LinkedIn image upload failed ({upload_status}): {body_text}");
}
Ok(image_urn)
}
/// Create a post with an attached image.
pub async fn create_post_with_image(
&self,
text: &str,
visibility: &str,
image_urn: &str,
scheduled_at: Option<&str>,
) -> anyhow::Result<String> {
let creds = self.get_credentials().await?;
let author_urn = format!("urn:li:person:{}", creds.person_id);
let lifecycle = if scheduled_at.is_some() {
"DRAFT"View on GitHub (pinned to 88bb9c8533)
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
Example fix
// before
let urn = client.upload_image(&bytes, "image/png").await?;
// after: validate payload first, retry only transient statuses
ensure_image_uploadable(&path)?;
let urn = match client.upload_image(&bytes, "image/png").await {
Ok(urn) => urn,
Err(e) if is_transient_http(&e.to_string()) => retry_with_backoff(|| client.upload_image(&bytes, "image/png")).await?,
Err(e) => return Err(e),
}; Defensive patterns
Strategy: retry
Validate before calling
fn ensure_image_uploadable(path: &Path) -> anyhow::Result<()> {
let meta = std::fs::metadata(path)?;
anyhow::ensure!(meta.len() <= 5 * 1024 * 1024, "image exceeds LinkedIn size cap");
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
anyhow::ensure!(matches!(ext, "jpg" | "jpeg" | "png" | "gif"), "unsupported media type: {ext}");
Ok(())
} Try / catch
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.
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- LinkedIn create_post_with_image failed ({status}): {body_tex
- LinkedIn image register failed ({status}): {body_text}
- Failed to initiate call: {}
- tenant_access_token request failed: status={status}, body={d
- bot info request failed after token refresh: status={retry_s
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/6cb2f19b5899f42e.
Report an issue: GitHub.