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

  1. Read the body_text embedded in the message and look up the LinkedIn error code (e.g. emptyAccessToken, mediaUploadFailed) before changing anything
  2. If 401/403: refresh the token (LinkedInClient::update_env_token) and rerun the full register + upload flow
  3. Verify the file is JPEG/PNG/GIF, under LinkedIn's limit, and that the media type string matches what was passed to registerUpload
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/6cb2f19b5899f42e. Report an issue: GitHub.