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

  1. Make the client send every file as '<topFolder>/<rest>' under one shared top-level folder name
  2. For HTML file inputs, add the webkitdirectory attribute so webkitRelativePath includes the folder prefix
  3. If you genuinely have multiple folders, upload them one per request, or nest them under one parent in the manifest
  4. 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

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


AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20). Data as JSON: /api/errors/dfb3e38740b95f31. Report an issue: GitHub.