FileMagic IOException on Excel Upload: The FileInputStream mark/reset Pitfall
FileMagic validation throws IOException on real Excel uploads because Tomcat's default FileInputStream lacks mark/reset support, unlike test ByteArrayInputStream; wrapping with BufferedInputStream fixes it, and regression tests must simulate real stream types to catch this.
Problem Phenomenon
An Excel batch upload interface failed in production with a uniform "read file failed" error. The exception log showed:
java.io.IOException: getFileMagic() only operates on streams which support mark(int)
at org.apache.poi.poifs.filesystem.FileMagic.valueOf(FileMagic.java:205)Background: What Is FileMagic
FileMagicis a file format detection utility from Apache POI (POI 3.17+), located in the main POI jar: org.apache.poi.poifs.filesystem.FileMagic. The project did not depend on POI directly; it came as a transitive dependency of EasyExcel 4.0.3 (confirmed via mvn dependency:tree):
com.alibaba:easyexcel:4.0.3
└── com.alibaba:easyexcel-core:4.0.3
├── org.apache.poi:poi:5.2.5 ← FileMagic lives here
└── org.apache.poi:poi-ooxml:5.2.5
└── org.apache.poi:poi-ooxml-lite:5.2.5Before parsing, the upload endpoint performed a magic-number check to prevent disguised malicious files, passing the multipart file's input stream directly to FileMagic:
try (InputStream is = file.getInputStream()) {
magic = FileMagic.valueOf(is); // blows up here
}
if (magic != FileMagic.OLE2 && magic != FileMagic.OOXML) {
throw new IllegalArgumentException("文件内容不是有效的Excel格式");
}Root Cause: Same Method, Different Stream Types Across Environments
FileMagic.valueOf()reads the file header to determine format, then calls reset() to rewind the stream for subsequent parsing. Therefore it requires the input stream to support mark(int) / reset() ; otherwise it throws IOException.
The stream returned by MultipartFile.getInputStream() depends on where the uploaded file is temporarily stored. After reaching the server, the file is either kept in memory or written to a disk temporary file, controlled by the configuration spring.servlet.multipart.file-size-threshold (default 0B, meaning every file goes to disk):
File smaller than threshold → stored in memory, returns ByteArrayInputStream File larger than threshold → written to disk, returns FileInputStream The difference in mark() / reset() support: ByteArrayInputStream holds data in a memory byte array; mark() records the current position and reset() can seek back to re-read. FileInputStream does not implement these methods (inherited default markSupported() returns false, reset() throws IOException); once bytes are read they cannot be rewound. FileMagic.valueOf() reads the first few bytes, then calls reset() to return to the stream start. With a FileInputStream the reset() fails, producing the observed
getFileMagic() only operates on streams which support mark(int)error.
Consequently, the validation fails on real HTTP requests but passes in unit tests because MockMultipartFile uses an in-memory byte array, masking the defect and creating a "local all green, production all red" illusion.
This pitfall is general: any code that makes implicit capability assumptions (mark/reset, re-reading, etc.) on a MultipartFile stream may not be caught by MockMultipartFile, since its behavior differs from a real servlet container.
Fix
Wrap the stream with a BufferedInputStream before validation:
try (InputStream is = new BufferedInputStream(file.getInputStream())) {
magic = FileMagic.valueOf(is);
} BufferedInputStreamimplements mark / reset (default 8 KB buffer, far larger than the magic-number length), making it a safe, general-purpose wrapper for "library needs to probe stream content" scenarios.
Regression Test: Must Replicate Real Container Stream Type
The original test used MockMultipartFile and could not expose the issue. A regression test must simulate the real environment by returning a stream that does not support mark:
@Test
void validateFileMagicSupportsNonMarkStream(@TempDir Path tempDir) throws Exception {
Path file = tempDir.resolve("test.xlsx");
Files.write(file, buildWorkbook()); // build a valid xlsx
MultipartFile multipart = Mockito.mock(MultipartFile.class);
Mockito.when(multipart.getInputStream())
.thenReturn(new FileInputStream(file.toFile())); // replicate Tomcat disk behavior
excelService.validateFileMagic(multipart); // threw IOException before fix
}Lesson: For stream-related code, regression tests must mimic the real environment's stream type; MockMultipartFile only verifies logic branches, not stream semantics. When necessary, add an integration verification with a real container (e.g., MockMvc + MockHttpServletRequestBuilder.multipart()).
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
