abhigyanpatwari/GitNexus · error · BadRequestError
Upload must be a single folder of files
Error message
Upload must be a single folder of files
What it means
The ingest endpoint accepts exactly one top-level folder: every file part's relative path must have at least two non-empty segments and all must share the same first segment (normalized NFC). The check runs during manifest validation, before any job is created — a bare root file would make the promote target a file, and a multi-top manifest would silently drop all but the first folder, so both are rejected outright.
Source
Thrown at gitnexus/src/server/upload-ingest.ts:253
const idx = fileIndex++;
const rel = manifest[idx];
let dest: string;
try {
dest = resolveContainedDest(stageRoot, rel);
// A folder upload is exactly one top-level directory: every entry must
// have ≥2 segments and share the same first segment. This rejects a
// bare file at the root (which would make the promote target a file)
// and a multi-top manifest (which would silently drop all but the
// first folder). Validated here, before any job is created.
const segs = String(rel)
.split('/')
.filter((s) => s.length > 0);
const firstSeg = (segs[0] ?? '').normalize('NFC');
if (!topLevelName) {
topLevelName = firstSeg;
}
if (segs.length < 2 || firstSeg !== topLevelName) {
throw new BadRequestError('Upload must be a single folder of files');
}
mkdirContained(stageRoot, dest, dirState);
} catch (err) {
stream.resume();
return fail(err as Error);
}
fileCount++;
const ws = fs.createWriteStream(dest, { flags: 'wx' });
const p = new Promise<void>((res, rej) => {
stream.on('data', (chunk: Buffer) => {
totalBytes += chunk.length;
if (totalBytes > limits.maxTotalBytes) {
stream.unpipe(ws);
ws.destroy();
rej(new BadRequestError('Upload exceeds total size limit', 413));
}
});
stream.on('limit', () => {View on GitHub (pinned to aac7515d2a)
Solutions
- Make the client send every file as '<topFolder>/<rest>' under one shared top-level folder name
- For HTML file inputs, add the webkitdirectory attribute so webkitRelativePath includes the folder prefix
- If you genuinely have multiple folders, upload them one per request, or nest them under one parent in the manifest
- Inspect the first file's relativePath on the client before starting the POST and fail fast with a clear message
Example fix
// before — bare file names, rejected
formData.append('files', file, file.name); // 'a.txt' → 1 segment
// after — keep the top-level folder in the relative path
const rel = file.webkitRelativePath || `upload/${file.name}`;
formData.append('files', file, rel); // 'upload/src/a.txt' Defensive patterns
Strategy: validation
Validate before calling
function isSingleFolder(files: { relativePath: string }[]): boolean {
if (files.length === 0) return false;
const top = files[0].relativePath.split('/').filter(Boolean)[0]?.normalize('NFC');
return files.every((f) => {
const segs = f.relativePath.split('/').filter(Boolean);
return segs.length >= 2 && segs[0].normalize('NFC') === top;
});
}
if (!isSingleFolder(files)) throw new Error('Select exactly one folder'); Prevention
- Use a directory picker (webkitdirectory) so every file carries '<folder>/...' in its relative path
- Never append files with bare filenames — always prefix the top-level folder name
- Upload multiple folders as separate requests, one per folder
When it happens
Trigger: A multipart upload where some file has relativePath 'README.md' (single segment), or where files arrive under two different roots like 'a/f1' and 'b/f2' (first segment differs from the already-locked topLevelName).
Common situations: A web uploader submitting dropped files without webkitdirectory, so each File contributes a bare filename; a client uploading two selected sibling folders in one request; a zip extractor that flattens away the top-level directory before upload.
Related errors
- Invalid upload path
- Upload must be a folder
- Uploaded folder has no usable name
- Upload path too long
- Upload path must not contain traversal segments
AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20).
Data as JSON: /api/errors/dfb3e38740b95f31.
Report an issue: GitHub.