Stirling-Tools/Stirling-PDF · warning · Error

Failed to download avatar

Error message

Failed to download avatar: ${response.status} ${response.statusText}

What it means

Thrown by downloadAndOptimizeAvatar() when fetch() to the OAuth provider's avatar URL returns a non-OK HTTP status. The error message includes the status code and status text for diagnosis. The fetch uses mode:'cors' and credentials:'omit' to comply with provider CORS policies. The caller syncOAuthAvatar() catches all errors and returns false, so this degrades gracefully to the user's existing picture or initials.

Solutions

  1. Check the specific HTTP status code in the error message (403, 404, 429, 5xx)
  2. For expired URLs, re-authenticate via OAuth to get a fresh avatar URL in user_metadata
  3. Add retry with exponential backoff for 429 and 5xx status codes
  4. Fall back to the existing stored avatar or user initials (syncOAuthAvatar already does this)

Example fix

// before
if (!response.ok) {
  throw new Error(`Failed to download avatar: ${response.status} ${response.statusText}`);
}

// after
if (!response.ok) {
  if (response.status === 429 || response.status >= 500) {
    await new Promise((r) => setTimeout(r, 1000));
    return downloadAndOptimizeAvatar(url); // retry once
  }
  throw new Error(`Failed to download avatar: ${response.status} ${response.statusText}`);
}
Defensive patterns

Strategy: fallback

Validate before calling

// Validate the avatar URL is reachable before full processing
// (syncOAuthAvatar already catches and degrades gracefully)
const url = getProviderAvatarUrl(user);
if (!url || !url.startsWith('https://')) {
  return false; // skip sync, use existing avatar
}

Try / catch

// syncOAuthAvatar already wraps in try/catch and returns false on failure:
// The caller should not throw — just check the boolean result:
const synced = await syncOAuthAvatar(user);
if (!synced) {
  // Gracefully degrade to existing avatar or initials — no error UI needed
}

Prevention

When it happens

Trigger: The avatar URL from OAuth provider metadata has expired or changed (GitHub avatar CDN URLs can rotate); the provider rate-limits unauthenticated image fetches (HTTP 429); the URL returns 403/404; CORS policy on the provider's CDN blocks the cross-origin request.

Common situations: Stale avatar URL cached in Supabase user_metadata after the provider rotated CDN links; GitHub/Google avatar CDN under rate limiting; provider image endpoint temporarily down; network proxy/firewall blocking the provider's image domain.

Related errors


AI-assisted analysis of Stirling-Tools/Stirling-PDF@9ef20dcab8 (2026-08-13). Data as JSON: /api/errors/3d71684bfaf6b026. Report an issue: GitHub.

Appendix: source

Thrown at frontend/editor/src/saas/services/avatarSyncService.ts:73

  }
}

/**
 * Download and optimize an avatar image
 * Resizes to 256x256 and converts to PNG format
 * @param url Avatar URL from OAuth provider
 * @returns Optimized image blob
 */
export async function downloadAndOptimizeAvatar(url: string): Promise<Blob> {
  try {
    // 1. Fetch image from provider URL
    const response = await fetch(url, {
      mode: "cors",
      credentials: "omit",
    });

    if (!response.ok) {
      throw new Error(
        `Failed to download avatar: ${response.status} ${response.statusText}`,
      );
    }

    const blob = await response.blob();

    // 2. Create image bitmap
    const img = await createImageBitmap(blob);

    // 3. Create canvas and draw scaled image
    const canvas = document.createElement("canvas");
    canvas.width = AVATAR_SIZE;
    canvas.height = AVATAR_SIZE;
    const ctx = canvas.getContext("2d");

    if (!ctx) {
      throw new Error("Failed to get canvas context");
    }

View on GitHub (pinned to 9ef20dcab8)