{"record":{"id":"451be854c2cb2fd3","repo":"zylon-ai/private-gpt","slug":"missing-files","errorCode":"MISSING_FILES","errorMessage":"At least one file is required","messagePattern":"At least one file is required","errorType":"error_code","errorClass":"SkillDomainError","httpStatus":null,"severity":"error","filePath":"private_gpt/server/skills/skills_files.py","lineNumber":46,"sourceCode":"            if raw is None:\n                continue\n            filename, content_type = _infer_file_meta(k, raw)\n            uploads.append(\n                UploadFile(\n                    file=BytesIO(raw),\n                    filename=filename,\n                    headers=Headers({\"content-type\": content_type}),\n                )\n            )\n    return uploads\n\n\nasync def stored_files_from_uploads(\n    uploads: list[UploadFile],\n    default_mime_type: str | None = \"application/octet-stream\",\n) -> list[StoredFile]:\n    if not uploads:\n        raise SkillDomainError(\n            SkillErrorCode.MISSING_FILES, \"At least one file is required\"\n        )\n\n    resolved: dict[str, StoredFile] = {}\n    for upload in uploads:\n        payload = await upload.read()\n\n        if _is_zip(upload):\n            for path, content in _extract_zip(upload, payload):\n                normalized = _normalize_path(path)\n                resolved[normalized] = StoredFile(\n                    path=normalized, content=content, mime_type=None\n                )\n        else:\n            name = _normalize_path(upload.filename or \"\")\n            resolved[name] = StoredFile(\n                path=name, content=payload, mime_type=upload.content_type\n            )","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/zylon-ai/private-gpt/blob/4a030776a31a901ad80b1bf4d7faa2c1a367efbb/private_gpt/server/skills/skills_files.py#L28-L64","documentation":"SkillDomainError with code MISSING_FILES, thrown by stored_files_from_uploads when the uploads list is empty. The skills upload pipeline requires at least one UploadFile (or one zip containing files) to build a skill version. It is a domain-level guard, not a framework error, and typically surfaces as an HTTP 401-mapped error response.","triggerScenarios":"Calling the multipart upload endpoint with no file parts at all, or a client that sends only form fields; also calling stored_files_from_uploads directly with an empty list (e.g. after filtering out zero-length parts upstream).","commonSituations":"HTML forms or scripts where the file input is optional and left blank; fetch/axios multipart bodies built from an empty array; test harnesses passing [] accidentally; upstream code dropping files due to size limits before the handler runs.","solutions":["Include at least one file part (typically a zip containing SKILL.md) in the multipart request.","If invoking stored_files_from_uploads programmatically, guard with `if not uploads: return []` (or raise your own clearer error) before calling.","Log the raw multipart body in a repro to confirm the file field name matches what the server expects."],"exampleFix":"# before\nfiles = await stored_files_from_uploads([])  # SkillDomainError\n# after\nif uploads:\n    files = await stored_files_from_uploads(uploads)\nelse:\n    raise HTTPException(422, 'Attach at least one file to the upload')","handlingStrategy":"validation","validationCode":"const form = new FormData();\n// ...\nif (![...form.keys()].some((k) => form.getAll(k).some((v) => v instanceof File))) {\n  throw new Error('Attach at least one file before submitting');\n}","typeGuard":"const hasFiles = (form: FormData): boolean =>\n  Array.from(form.getAll('files')).some((v) => v instanceof File && v.size >= 0);","tryCatchPattern":null,"preventionTips":["Disable the submit button until at least one file part is attached.","Name the file field exactly what the endpoint expects and log the multipart keys in dev builds.","Never call stored_files_from_uploads with a possibly-empty list without an explicit check."],"tags":["upload","multipart","skills","validation","domain-error"],"backgroundTag":null,"analyzedSha":"4a030776a31a901ad80b1bf4d7faa2c1a367efbb","analyzedAt":"2026-08-15T03:51:26.951Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}