{"record":{"id":"84050f92a58171b4","repo":"siyuan-note/siyuan","slug":"field-srcs-must-not-be-empty","errorCode":null,"errorMessage":"Field [srcs] must not be empty","messagePattern":"Field \\[srcs\\] must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/apicontract/file_input.go","lineNumber":21,"sourceCode":"import (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n)\n\nfunc decodeCopyFiles(reader io.Reader, path string, trim bool) (request CopyFilesRequest, err error) {\n\tfields, err := blockRequestFields(reader, path)\n\tif err != nil {\n\t\treturn request, err\n\t}\n\tsrcs, err := legacyField[[]json.RawMessage](fields, \"srcs\", \"Array\", true)\n\tif err != nil {\n\t\treturn request, err\n\t}\n\tif len(srcs) == 0 {\n\t\treturn request, fmt.Errorf(\"Field [srcs] must not be empty\")\n\t}\n\trequest.DestDir, err = legacyField[string](fields, \"destDir\", \"String\", true)\n\tif err != nil {\n\t\treturn request, err\n\t}\n\tfor _, raw := range srcs {\n\t\tvar src string\n\t\tif bytes.Equal(raw, []byte(\"null\")) || json.Unmarshal(raw, &src) != nil {\n\t\t\treturn request, fmt.Errorf(\"Field [srcs]: each element should be of type [String]\")\n\t\t}\n\t\tif trim {\n\t\t\tsrc = strings.TrimSpace(src)\n\t\t\tif src == \"\" {\n\t\t\t\treturn request, fmt.Errorf(\"Field [srcs]: path must not be empty\")\n\t\t\t}\n\t\t}\n\t\trequest.Srcs = append(request.Srcs, src)\n\t}","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/apicontract/file_input.go#L3-L39","documentation":"decodeCopyFiles in kernel/apicontract/file_input.go requires the \"srcs\" array field to be present and contain at least one entry; an empty array fails with \"Field [srcs] must not be empty\". Copying files with no sources is a no-op the API refuses rather than silently succeeding.","triggerScenarios":"Calling the copy-files API (legacyField[[]json.RawMessage] with required Array) with srcs: [] or srcs omitted-and-defaulted to empty after successful decoding.","commonSituations":"File manager integrations computing the source list from a selection that ended up empty; filtering out all candidate files before the call; loops that batch-copy and hit the final empty batch.","solutions":["Ensure srcs contains at least one path before calling the API","Skip the API call entirely when the source list is empty client-side","Check the selection/filter logic that builds srcs","Include destDir alongside a non-empty srcs list as the endpoint expects both"],"exampleFix":"// before\nfetchPost(\"/api/file/copyFiles\", { srcs: [], destDir })\n// after\nif (!srcs.length) return; fetchPost(\"/api/file/copyFiles\", { srcs, destDir })","handlingStrategy":"validation","validationCode":"function requireNonEmptySrcs(srcs) {\n  if (!Array.isArray(srcs) || srcs.length === 0) throw new Error(\"srcs must contain at least one path\");\n}\nrequireNonEmptySrcs(srcs);","typeGuard":"const hasSrcs = (p) => Array.isArray(p.srcs) && p.srcs.length > 0;","tryCatchPattern":"try { await copyFiles({ srcs, destDir }); } catch (e) { if (String(e).includes(\"srcs] must not be empty\")) return; /* skip empty batch */ throw e; }","preventionTips":["Check selection size before calling copy APIs","Skip empty batches instead of issuing calls","Verify the filter that builds srcs never removes everything silently","Unit-test payload builders against empty-selection edge cases"],"tags":["api","validation","empty-array","file-operations"],"backgroundTag":"empty-required-field","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}