elastic/elasticsearch · error · IOException

md5 checksum mismatch, expected [{}], actual [{}]

Error message

md5 checksum mismatch, expected [{}], actual [{}]

What it means

Thrown by GeoIpDownloader.indexChunks (the non-enterprise legacy path) after indexing all chunks of a database and flushing/refreshing the .geoip database index. The md5 of the streamed bytes is compared against the expected md5 from the provider; a mismatch means the downloaded bytes do not match what the provider advertised. IOException, so it is treated as a retryable download failure.

Source

Thrown at modules/ip-location/src/main/java/org/elasticsearch/ingest/geoip/GeoIpDownloader.java:274

            md.update(buf);
            IndexRequest indexRequest = new IndexRequest(DATABASES_INDEX).id(name + "_" + chunk + "_" + timestamp)
                .create(true)
                .source(XContentType.SMILE, "name", name, "chunk", chunk, "data", buf);
            client.index(indexRequest).actionGet();
            chunk++;
        }

        // May take some time before automatic flush kicks in:
        // (otherwise the translog will contain large documents for some time without good reason)
        FlushRequest flushRequest = new FlushRequest(DATABASES_INDEX);
        client.admin().indices().flush(flushRequest).actionGet();
        // Ensure that the chunk documents are visible:
        RefreshRequest refreshRequest = new RefreshRequest(DATABASES_INDEX);
        client.admin().indices().refresh(refreshRequest).actionGet();

        String actualMd5 = MessageDigests.toHexString(md.digest());
        if (Objects.equals(expectedMd5, actualMd5) == false) {
            throw new IOException("md5 checksum mismatch, expected [" + expectedMd5 + "], actual [" + actualMd5 + "]");
        }
        return chunk;
    }

    // visible for testing
    static byte[] getChunk(InputStream is) throws IOException {
        byte[] buf = new byte[MAX_CHUNK_SIZE];
        int chunkSize = 0;
        while (chunkSize < MAX_CHUNK_SIZE) {
            int read = is.read(buf, chunkSize, MAX_CHUNK_SIZE - chunkSize);
            if (read == -1) {
                break;
            }
            chunkSize += read;
        }
        if (chunkSize < MAX_CHUNK_SIZE) {
            buf = Arrays.copyOf(buf, chunkSize);
        }

View on GitHub (pinned to db6a809a66)

Solutions

  1. Allow the next scheduled download to retry; transient mismatches usually clear.
  2. Verify the network path to the public geoip database host is clean (no intercepting proxy).
  3. Compare expected vs actual md5 in the logs: a wildly different actual suggests an error body; a near miss suggests truncation.
  4. Re-trigger the download manually or restart the ingest node if the stale-expected-md5 condition persists.
Defensive patterns

Strategy: retry

Try / catch

try {
    downloader.indexChunks(name, is, chunk, expectedMd5, ts);
} catch (IOException e) {
    if (e.getMessage().startsWith("md5 checksum mismatch")) {
        // discard partial chunks, re-fetch, retry once; then let the scheduler handle it
        throw e;
    }
    throw e;
}

Prevention

When it happens

Trigger: GeoIpDownloader download flow -> indexChunks computes MessageDigests.toHexString(md.digest()) and compares to expectedMd5; mismatch throws IOException at the Objects.equals check.

Common situations: Truncated or corrupted download; proxy returning a partial/error body; unstable connection to the public geoip endpoint; mismatch between expected md5 (stale) and the freshly served file.

Related errors


AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12). Data as JSON: /api/errors/9ef112291d13f60c. Report an issue: GitHub.