{"record":{"id":"940e687832c546ec","repo":"spring-projects/spring-ai","slug":"invalid-filename-for-file-null-or-blank","errorCode":null,"errorMessage":"Invalid filename for file '': null or blank","messagePattern":"Invalid filename for file '': null or blank","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java","lineNumber":151,"sourceCode":"\t\t\t\tPath filePath = resolveSafeChildPath(targetDir, metadata.filename(), fileId);\n\t\t\t\tFiles.write(filePath, content);\n\t\t\t\tsavedPaths.add(filePath);\n\t\t\t}\n\t\t}\n\n\t\treturn savedPaths;\n\t}\n\n\t/**\n\t * Validate an API-provided filename and resolve it to a child of {@code targetDir}.\n\t * Rejects null/blank names, absolute paths, names containing path separators or\n\t * {@code .}/{@code ..} segments, and names that resolve outside {@code targetDir}.\n\t * Filenames come from model-influenced API metadata and must not be trusted as safe\n\t * path components.\n\t */\n\tstatic Path resolveSafeChildPath(Path targetDir, @Nullable String rawName, String fileId) throws IOException {\n\t\tif (rawName == null || rawName.isBlank()) {\n\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': null or blank\");\n\t\t}\n\t\tPath name;\n\t\ttry {\n\t\t\tname = Path.of(rawName);\n\t\t}\n\t\tcatch (InvalidPathException ex) {\n\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': \" + rawName, ex);\n\t\t}\n\t\tif (name.isAbsolute() || name.getRoot() != null) {\n\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': absolute path '\" + rawName + \"'\");\n\t\t}\n\t\tif (name.getNameCount() != 1) {\n\t\t\tthrow new IOException(\n\t\t\t\t\t\"Invalid filename for file '\" + fileId + \"': must be a single path segment '\" + rawName + \"'\");\n\t\t}\n\t\tString only = name.getName(0).toString();\n\t\tif (only.equals(\".\") || only.equals(\"..\")) {\n\t\t\tthrow new IOException(\"Invalid filename for file '\" + fileId + \"': '\" + rawName + \"'\");","sourceCodeStart":133,"sourceCodeEnd":169,"githubUrl":"https://github.com/spring-projects/spring-ai/blob/98a7beda4f29d80a71c5837eb4053b03a93a46f7/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicSkillsResponseHelper.java#L133-L169","documentation":"AnthropicSkillsResponseHelper.resolveSafeChildPath validates filenames returned by the model/API before using them as path components. If the raw filename is null or blank, it throws IOException to prevent writing to an empty/invalid path. Filenames come from model-influenced metadata and are explicitly untrusted, so every rule (non-blank, relative, single segment, inside targetDir) is enforced with an IOException.","triggerScenarios":"Processing a skills/file API response whose file entry has a null or empty/whitespace-only name field; deserialization dropping the name property; resolving a child path for a file entry that only carries a fileId without a name.","commonSituations":"API responses with missing 'name' JSON fields mapped to null; upstream schema changes renaming the field; partial responses where a file was created without a name; mapping code that copies only id but not name.","solutions":["Skip or reject the file entry and log the fileId instead of attempting to resolve a path for it.","Verify your response-mapping class actually binds the filename field (check @JsonProperty/field names against the API response).","Fetch fresh file metadata for the fileId to obtain a valid name before resolving paths.","Add a fallback name (e.g. derived from fileId) only if it is safe and acceptable for your storage layout."],"exampleFix":"// before\nPath p = resolveSafeChildPath(targetDir, fileEntry.name(), fileEntry.id());\n// after\nif (fileEntry.name() == null || fileEntry.name().isBlank()) {\n    log.warn(\"Skipping file {} with missing name\", fileEntry.id());\n    return null;\n}\nPath p = resolveSafeChildPath(targetDir, fileEntry.name(), fileEntry.id());","handlingStrategy":"try-catch","validationCode":"if (entry.name() == null || entry.name().isBlank()) {\n    log.warn(\"Skipping file {} with null/blank name\", entry.id());\n    return;\n}","typeGuard":"static boolean hasUsableName(AnalyzedFile entry) {\n    return entry.name() != null && !entry.name().isBlank();\n}","tryCatchPattern":"try {\n    Path p = AnthropicSkillsResponseHelper.resolveSafeChildPath(targetDir, rawName, fileId);\n    Files.copy(in, p);\n} catch (IOException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"null or blank\")) {\n        log.warn(\"File {} has no usable filename; skipped\", fileId);\n    } else throw e;\n}","preventionTips":["Validate that response-mapping binds the filename field before processing entries.","Treat missing names as skip-and-log, never as empty-string paths.","Re-fetch metadata when name is absent but the file is required."],"tags":["java","spring-ai","anthropic","skills","invalid-filename","path-safety"],"backgroundTag":"invalid-argument-format","analyzedSha":"98a7beda4f29d80a71c5837eb4053b03a93a46f7","analyzedAt":"2026-09-11T14:15:49.441Z","contentChangedAt":"2026-09-11T14:15:49.441Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}