apache/iceberg · error · IOException

Writing to closed stream

Error message

Writing to closed stream

What it means

AesGcmOutputStream refuses writes after close() has been called. Iceberg's AES-GCM stream writer finalizes the last encrypted block and writes trailers during close; any subsequent write would corrupt the ciphertext, so it throws IOException immediately.

Source

Thrown at core/src/main/java/org/apache/iceberg/encryption/AesGcmOutputStream.java:74

    this.cipherBlock = new byte[Ciphers.CIPHER_BLOCK_SIZE];
    this.positionInPlainBlock = 0;
    this.currentBlockIndex = 0;
    this.isHeaderWritten = false;
    this.lastBlockWritten = false;
    this.isClosed = false;
    this.finalPosition = 0;
  }

  @Override
  public void write(int b) throws IOException {
    singleByte[0] = (byte) (b & 0x000000FF);
    write(singleByte);
  }

  @Override
  public void write(byte[] b, int off, int len) throws IOException {
    if (isClosed) {
      throw new IOException("Writing to closed stream");
    }

    if (!isHeaderWritten) {
      writeHeader();
    }

    if (b.length - off < len) {
      throw new IOException(
          "Insufficient bytes in buffer: " + b.length + " - " + off + " < " + len);
    }

    int remaining = len;
    int offset = off;

    while (remaining > 0) {
      int freeBlockBytes = plainBlock.length - positionInPlainBlock;
      int toWrite = Math.min(freeBlockBytes, remaining);

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Remove the extra write after close, or move it before close()
  2. Stop double-closing: rely on try-with-resources and don't call close() manually too
  3. If the stream may be closed, create a new AesGcmOutputStream instead of reusing the closed one
  4. Guard writes with an isOpen/closed flag in the owning code

Example fix

// before
out.close();
out.write(footerBytes);
// after
out.write(footerBytes);
out.close();
Defensive patterns

Strategy: try-catch

Validate before calling

if (!streamClosed) { out.write(data); }

Try / catch

try {
  out.write(data);
} catch (IOException e) {
  if ("Writing to closed stream".equals(e.getMessage())) {
    out = createNewStream(); // recreate instead of reusing
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling write(int), write(byte[]) or write(byte[],int,int) on an AesGcmOutputStream after close() has already been invoked.

Common situations: Double-closing a stream via try-with-resources plus explicit close(), then writing a final marker; writing to a cached stream instance that a previous code path already closed; retry logic that reuses a closed output stream after a failure.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/b62fc6801a4ab757. Report an issue: GitHub.