Tencent/tinker · error · ZipException

File too short to be a zip file: ${raf.length()}

Error message

File too short to be a zip file: ${raf.length()}

What it means

In readCentralDir, TinkerZipFile first computes raf.length() - ENDHDR (ENDHDR is the 22-byte EOCD record size); if the file is shorter than that, there is no room for even a minimal End Of Central Directory record, so it throws ZipException('File too short to be a zip file: <length>'). The length is included so you can immediately distinguish a truncated download from other corruption.

Source

Thrown at third-party/tinker-ziputils/src/main/java/com/tencent/tinker/ziputils/ziputil/TinkerZipFile.java:422

     *
     * <p>The central directory can be followed by a variable-length comment
     * field, so we have to scan through it backwards.  The comment is at
     * most 64K, plus we have 18 bytes for the end-of-central-dir stuff
     * itself, plus apparently sometimes people throw random junk on the end
     * just for the fun of it.
     *
     * <p>This is all a little wobbly.  If the wrong value ends up in the EOCD
     * area, we're hosed. This appears to be the way that everybody handles
     * it though, so we're in good company if this fails.
     */
    private void readCentralDir() throws IOException {
        // Scan back, looking for the End Of Central Directory field. If the zip file doesn't
        // have an overall comment (unrelated to any per-entry comments), we'll hit the EOCD
        // on the first try.
        // No need to synchronize raf here -- we only do this when we first open the zip file.
        long scanOffset = raf.length() - ENDHDR;
        if (scanOffset < 0) {
            throw new ZipException("File too short to be a zip file: " + raf.length());
        }

        raf.seek(0);
        final int headerMagic = Integer.reverseBytes(raf.readInt());
        if (headerMagic != LOCSIG) {
            throw new ZipException("Not a zip archive");
        }

        long stopOffset = scanOffset - 65536;
        if (stopOffset < 0) {
            stopOffset = 0;
        }

        while (true) {
            raf.seek(scanOffset);
            if (Integer.reverseBytes(raf.readInt()) == ENDSIG) {
                break;
            }

View on GitHub (pinned to 1b7ea02c23)

Solutions

  1. Check the reported length: 0 or a few hundred bytes almost always means a failed download — re-fetch the file.
  2. Validate the downloaded artifact's size/ checksum against the server-provided Content-Length before opening.
  3. Guard with a minimum-size check (>= 22 bytes plus entry data) in your pipeline and reject earlier with a clearer message.

Example fix

// before
TinkerZipFile zf = new TinkerZipFile(new File(path)); // may be a 0-byte stub

// after
File f = new File(path);
if (f.length() < 22) {
    throw new IOException("Incomplete download: " + path + " is only " + f.length() + " bytes");
}
TinkerZipFile zf = new TinkerZipFile(f);
Defensive patterns

Strategy: validation

Validate before calling

static final long MIN_ZIP_BYTES = 22; // ENDHDR

if (file.length() < MIN_ZIP_BYTES) {
    throw new IOException("File too small to be a zip (" + file.length() + " bytes): " + file);
}
TinkerZipFile zf = new TinkerZipFile(file);

Prevention

When it happens

Trigger: Constructing TinkerZipFile on a file smaller than 22 bytes: empty files (0 bytes), 1-byte stubs, HTML error pages saved with a .zip extension, or a download that created the file but wrote nothing.

Common situations: HTTP error responses saved as the payload; a downloader creating the target file then failing before writing; passing a directory or wrong path that resolves to a tiny placeholder; test fixtures that are empty by accident.

Related errors


AI-assisted analysis of Tencent/tinker@1b7ea02c23 (2026-08-14). Data as JSON: /api/errors/8bd3d2ead41d2a40. Report an issue: GitHub.