Repository navigation
Title: detectMimeType(File) / detectMimeType(Path) leak a file descriptor (AsynchronousFileChannel never closed) #20
Description
Generated by Claude:
Affected versions: 1.0.2 through 2.0.0 (and current master)
Summary
MimeTypeDetector.detectMimeType(File) and detectMimeType(Path) open an AsynchronousFileChannel that is never closed. Every call leaks one file descriptor, held until the channel is eventually garbage-collected. Under sustained use this exhausts the process's open-file limit.
Details
In detectMimeTypeAsync(Path):
AsynchronousFileChannel channel;
try {
channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); // FD opened
} catch (IOException e) {
futureBytes.completeExceptionally(new GetBytesException(e));
return futureBytes;
}
final byte[] wrappedBytes = new byte[getMaxGetBytesLength()];
final ByteBuffer buf = ByteBuffer.wrap(wrappedBytes);
channel.read(buf, 0, futureBytes, new CompletionHandler<Integer, CompletableFuture<byte[]>>() {
@Override public void completed(Integer nBytes, CompletableFuture<byte[]> f) {
// ... completes the future; channel is never closed
}
@Override public void failed(Throwable exc, CompletableFuture<byte[]> f) {
f.completeExceptionally(new GetBytesException(exc)); // channel is never closed
}
});
return futureBytes;The channel is referenced only by open and read — there is no channel.close() in completed(), in failed(), in a whenComplete, or in any finally. Once the supplier lambda returns, the only reference is captured inside the completion handler, and the descriptor stays open until GC finalizes the channel.
The detectMimeType(String, InputStream) overload is unaffected, since the caller owns and closes the stream.
Reproduction
MimeTypeDetector detector = new MimeTypeDetector();
Path p = Files.createTempFile("leak", ".txt");
Files.write(p, "hello world".getBytes());
for (int i = 0; i < 5000; i++) {
detector.detectMimeType(p);
}
// On Linux, `ls /proc/<pid>/fd | wc -l` climbs steadily;
// eventually: java.io.IOException: Too many open filesImpact
In a long-running server that sniffs content types on a request path, this steadily consumes file descriptors and eventually causes Too many open files, taking the process down. We hit exactly this in production (Tomcat reaching its 4096 limit); lsof showed thousands of leaked read handles to the sniffed files, decoupled from any live request.
Suggested fix
Close the channel once the read completes, in both callbacks:
channel.read(buf, 0, futureBytes, new CompletionHandler<Integer, CompletableFuture<byte[]>>() {
@Override public void completed(Integer nBytes, CompletableFuture<byte[]> f) {
closeQuietly(channel);
if (nBytes == wrappedBytes.length) {
f.complete(wrappedBytes);
} else {
if (nBytes == -1) nBytes = 0;
f.complete(Arrays.copyOf(wrappedBytes, nBytes));
}
}
@Override public void failed(Throwable exc, CompletableFuture<byte[]> f) {
closeQuietly(channel);
f.completeExceptionally(new GetBytesException(exc));
}
});(where closeQuietly swallows/ignores IOException from channel.close()). Equivalently, attach futureBytes.whenComplete((r, t) -> closeQuietly(channel)) before returning.
Activity
melloware commentedon Jul 21, 2026
@arneevertsson please submit a PR!