vercel-labs/skills · error

Downloaded URL is empty

Error message

Downloaded URL is empty

What it means

Raised by downloadSource() when the HTTP download completes but the resulting file on disk is zero bytes. The library treats an empty body as a hard failure because neither SKILL.md parsing nor archive extraction can succeed on it.

Source

Thrown at src/download-source.ts:270

  const { readdir } = await import('node:fs/promises');
  const entries = await readdir(dir, { withFileTypes: true });
  const visibleEntries = entries.filter((entry) => entry.name !== '__MACOSX');
  if (visibleEntries.length !== 1 || !visibleEntries[0]!.isDirectory()) return null;
  return join(dir, visibleEntries[0]!.name);
}

export async function downloadSource(url: string): Promise<DownloadedSource> {
  const limits = getDownloadLimits();
  const tempDir = await mkdtemp(join(tmpdir(), 'skills-download-'));
  const downloadedFile = join(tempDir, 'source.download');
  const extractDir = join(tempDir, 'extract');

  try {
    await downloadToFile(url, downloadedFile, limits);

    const downloadedStats = await stat(downloadedFile);
    if (downloadedStats.size === 0) {
      throw new Error('Downloaded URL is empty');
    }

    if (await isValidSkillMarkdown(downloadedFile)) {
      const skillDir = join(tempDir, 'skill');
      await mkdir(skillDir, { recursive: true });
      await writeFile(join(skillDir, 'SKILL.md'), await readFile(downloadedFile));
      return { rootDir: skillDir, tempDir, kind: 'skill-md' };
    }

    await mkdir(extractDir, { recursive: true });
    if (await tryExtractArchive(downloadedFile, extractDir, limits)) {
      const rootDir = (await getSingleTopLevelDirectory(extractDir)) ?? extractDir;
      return { rootDir, tempDir, kind: 'archive' };
    }

    throw new Error('Downloaded URL is not a valid SKILL.md file or supported archive');
  } catch (error) {
    await rm(tempDir, { recursive: true, force: true }).catch(() => {});

View on GitHub (pinned to 435076e789)

Solutions

  1. curl -IL <url> the exact URL and check the final status code, redirect chain, and Content-Length
  2. If it's a private repo asset, provide a token or use a public raw URL
  3. Point at the raw file directly (e.g. raw.githubusercontent.com/.../SKILL.md) rather than an HTML page
  4. Retry with a stable network if the transfer was truncated

Example fix

# before
skills add https://example.com/my-skill   # redirects to empty login page
# after
skills add https://raw.githubusercontent.com/org/repo/main/SKILL.md
Defensive patterns

Strategy: retry

Validate before calling

const size = await fetch(url, { method: 'HEAD' }).then((r) => Number(r.headers.get('content-length') ?? 0));
if (size <= 0) throw new Error(`URL ${url} returns empty body; refusing to download`);

Try / catch

let lastErr;
for (let attempt = 0; attempt < 3; attempt++) {
  try { return await downloadSource(url); }
  catch (e) {
    lastErr = e;
    if (!(e instanceof Error && /Downloaded URL is empty/.test(e.message))) throw e;
    await new Promise((r) => setTimeout(r, 500 * 2 ** attempt));
  }
}
throw lastErr;

Prevention

When it happens

Trigger: downloadToFile(url) succeeds (2xx) but the server returns an empty body — common with misconfigured redirects, 204/200-with-no-content endpoints, auth walls that redirect to a blank page, or truncated connections.

Common situations: Passing a URL that redirects (auth required, changed repo layout) to 'skills add <url>'; GitHub raw URLs for files that were renamed/deleted; CDN/proxy returning empty 200; typos in the URL path.

Related errors


AI-assisted analysis of vercel-labs/skills@435076e789 (2026-08-28). Data as JSON: /api/errors/d918410f1e9ae441. Report an issue: GitHub.