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
- curl -IL <url> the exact URL and check the final status code, redirect chain, and Content-Length
- If it's a private repo asset, provide a token or use a public raw URL
- Point at the raw file directly (e.g. raw.githubusercontent.com/.../SKILL.md) rather than an HTML page
- 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
- Verify URLs with curl -IL before wiring them into config
- Prefer raw content URLs (raw.githubusercontent.com) over HTML pages
- Add a HEAD-request size check before downloading in your own tooling
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
- Download response has no body
- Download failed with HTTP ${response.status}
- Download is larger than ${limits.downloadMaxBytes} bytes. Se
- Archive contains unsafe path: ${entryPath}
- Downloaded URL is not a valid SKILL.md file or supported arc
AI-assisted analysis of vercel-labs/skills@435076e789 (2026-08-28).
Data as JSON: /api/errors/d918410f1e9ae441.
Report an issue: GitHub.