Skip to content

Title: detectMimeType(File) / detectMimeType(Path) leak a file descriptor (AsynchronousFileChannel never closed) #20

Open

Description

@arneevertsson

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 files

Impact

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

  1. melloware commented on Jul 21, 2026

    @melloware
    Contributor

    @arneevertsson please submit a PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions