abhigyanpatwari/GitNexus · error · BadRequestError

Upload must be a folder

Error message

Upload must be a folder

What it means

The upload handler joins stageRoot with the manifest's topLevelName and stats it; webkitRelativePath-based folder uploads always prefix entries with the picked folder, so the joined path must be a directory. If stat fails or reports a non-directory, the request is rejected with BadRequestError 400 'Upload must be a folder' BEFORE taking the single analysis slot, so a malformed upload cannot wedge the server.

Source

Thrown at gitnexus/src/server/analyze-upload.ts:96

      const baseName = deriveUploadName(result.topLevelName);
      if (!baseName) {
        throw new BadRequestError('Uploaded folder has no usable name');
      }

      // webkitRelativePath prefixes every entry with the picked folder, so the
      // real repo root is stageRoot/<topLevelName>. Validate it is a directory
      // BEFORE taking the single analysis slot — a malformed (non-folder)
      // upload must not be able to occupy the slot.
      const innerRoot = path.join(result.stageRoot, result.topLevelName);
      let innerIsDir = false;
      try {
        innerIsDir = (await fsp.stat(innerRoot)).isDirectory();
      } catch {
        innerIsDir = false;
      }
      if (!innerIsDir) {
        throw new BadRequestError('Upload must be a folder');
      }

      const finalName = await pickAvailableName(baseName);
      const finalDir = getUploadDir(finalName);

      // createJob occupies the single analysis slot (throws 'already in
      // progress' → 409). From here on, ANY error before launch MUST release
      // the slot via failJob in the catch, or the server wedges all analyses.
      let job: UploadJobRef;
      try {
        job = deps.createJob({ repoPath: finalDir });
      } catch (err) {
        const msg = err instanceof Error ? err.message : '';
        if (msg.includes('already in progress')) {
          throw new BadRequestError(msg, 409);
        }
        throw err;
      }

View on GitHub (pinned to aac7515d2a)

Solutions

  1. Use <input type='file' webkitdirectory> (folder picker) so every entry's relative path starts with the folder name
  2. Ensure custom upload clients keep the top-level folder prefix on every part's path
  3. Upload a directory, not individual files

Example fix

<!-- before -->
<input type="file" id="f">

<!-- after -->
<input type="file" id="f" webkitdirectory>
Defensive patterns

Strategy: validation

Validate before calling

function uploadLooksLikeFolder(files) {
  return files.length > 0 && files.every((f) => (f.webkitRelativePath || '').includes('/'));
}

Type guard

function isFolderUpload(fileList) {
  return [...fileList].every((f) => typeof f.webkitRelativePath === 'string' && f.webkitRelativePath.split('/').length >= 2);
}

Try / catch

try { await postUpload(formData); }
catch (e) {
  if (e.status === 400 && e.message === 'Upload must be a folder') showHint('Select a folder, not individual files');
  else throw e;
}

Prevention

When it happens

Trigger: POST /api/analyze/upload whose multipart manifest's top-level entry is a file: a drag-dropped single file, or a custom client that posts entries without the folder prefix the browser's folder picker would add.

Common situations: Using a plain <input type='file'> instead of one with the webkitdirectory attribute, so relative paths lack a folder root; hand-rolled upload clients that strip the top-level folder from paths; drag-and-drop of a file rather than a folder.

Related errors


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