{"record":{"id":"b306c93bff52a170","repo":"abhigyanpatwari/GitNexus","slug":"upload-must-be-a-folder","errorCode":null,"errorMessage":"Upload must be a folder","messagePattern":"Upload must be a folder","errorType":"http","errorClass":"BadRequestError","httpStatus":400,"severity":"error","filePath":"gitnexus/src/server/analyze-upload.ts","lineNumber":96,"sourceCode":"\n      const baseName = deriveUploadName(result.topLevelName);\n      if (!baseName) {\n        throw new BadRequestError('Uploaded folder has no usable name');\n      }\n\n      // webkitRelativePath prefixes every entry with the picked folder, so the\n      // real repo root is stageRoot/<topLevelName>. Validate it is a directory\n      // BEFORE taking the single analysis slot — a malformed (non-folder)\n      // upload must not be able to occupy the slot.\n      const innerRoot = path.join(result.stageRoot, result.topLevelName);\n      let innerIsDir = false;\n      try {\n        innerIsDir = (await fsp.stat(innerRoot)).isDirectory();\n      } catch {\n        innerIsDir = false;\n      }\n      if (!innerIsDir) {\n        throw new BadRequestError('Upload must be a folder');\n      }\n\n      const finalName = await pickAvailableName(baseName);\n      const finalDir = getUploadDir(finalName);\n\n      // createJob occupies the single analysis slot (throws 'already in\n      // progress' → 409). From here on, ANY error before launch MUST release\n      // the slot via failJob in the catch, or the server wedges all analyses.\n      let job: UploadJobRef;\n      try {\n        job = deps.createJob({ repoPath: finalDir });\n      } catch (err) {\n        const msg = err instanceof Error ? err.message : '';\n        if (msg.includes('already in progress')) {\n          throw new BadRequestError(msg, 409);\n        }\n        throw err;\n      }","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/server/analyze-upload.ts#L78-L114","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use <input type='file' webkitdirectory> (folder picker) so every entry's relative path starts with the folder name","Ensure custom upload clients keep the top-level folder prefix on every part's path","Upload a directory, not individual files"],"exampleFix":"<!-- before -->\n<input type=\"file\" id=\"f\">\n\n<!-- after -->\n<input type=\"file\" id=\"f\" webkitdirectory>","handlingStrategy":"validation","validationCode":"function uploadLooksLikeFolder(files) {\n  return files.length > 0 && files.every((f) => (f.webkitRelativePath || '').includes('/'));\n}","typeGuard":"function isFolderUpload(fileList) {\n  return [...fileList].every((f) => typeof f.webkitRelativePath === 'string' && f.webkitRelativePath.split('/').length >= 2);\n}","tryCatchPattern":"try { await postUpload(formData); }\ncatch (e) {\n  if (e.status === 400 && e.message === 'Upload must be a folder') showHint('Select a folder, not individual files');\n  else throw e;\n}","preventionTips":["Use <input type='file' webkitdirectory> so the browser prefixes every entry with the folder","In custom clients, always write the top-level folder as the first path segment of each part","Disable the submit button until isFolderUpload(files) is true"],"tags":["upload","multipart","http-400","validation"],"backgroundTag":"invalid-upload-format","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T14:17:55.899Z"}