{"record":{"id":"b62fc6801a4ab757","repo":"apache/iceberg","slug":"writing-to-closed-stream","errorCode":null,"errorMessage":"Writing to closed stream","messagePattern":"Writing to closed stream","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/org/apache/iceberg/encryption/AesGcmOutputStream.java","lineNumber":74,"sourceCode":"    this.cipherBlock = new byte[Ciphers.CIPHER_BLOCK_SIZE];\n    this.positionInPlainBlock = 0;\n    this.currentBlockIndex = 0;\n    this.isHeaderWritten = false;\n    this.lastBlockWritten = false;\n    this.isClosed = false;\n    this.finalPosition = 0;\n  }\n\n  @Override\n  public void write(int b) throws IOException {\n    singleByte[0] = (byte) (b & 0x000000FF);\n    write(singleByte);\n  }\n\n  @Override\n  public void write(byte[] b, int off, int len) throws IOException {\n    if (isClosed) {\n      throw new IOException(\"Writing to closed stream\");\n    }\n\n    if (!isHeaderWritten) {\n      writeHeader();\n    }\n\n    if (b.length - off < len) {\n      throw new IOException(\n          \"Insufficient bytes in buffer: \" + b.length + \" - \" + off + \" < \" + len);\n    }\n\n    int remaining = len;\n    int offset = off;\n\n    while (remaining > 0) {\n      int freeBlockBytes = plainBlock.length - positionInPlainBlock;\n      int toWrite = Math.min(freeBlockBytes, remaining);\n","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/core/src/main/java/org/apache/iceberg/encryption/AesGcmOutputStream.java#L56-L92","documentation":"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.","triggerScenarios":"Calling write(int), write(byte[]) or write(byte[],int,int) on an AesGcmOutputStream after close() has already been invoked.","commonSituations":"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.","solutions":["Remove the extra write after close, or move it before close()","Stop double-closing: rely on try-with-resources and don't call close() manually too","If the stream may be closed, create a new AesGcmOutputStream instead of reusing the closed one","Guard writes with an isOpen/closed flag in the owning code"],"exampleFix":"// before\nout.close();\nout.write(footerBytes);\n// after\nout.write(footerBytes);\nout.close();","handlingStrategy":"try-catch","validationCode":"if (!streamClosed) { out.write(data); }","typeGuard":null,"tryCatchPattern":"try {\n  out.write(data);\n} catch (IOException e) {\n  if (\"Writing to closed stream\".equals(e.getMessage())) {\n    out = createNewStream(); // recreate instead of reusing\n  } else { throw e; }\n}","preventionTips":["Use try-with-resources exclusively; never close manually as well","Do not cache and reuse output stream instances across operations","Buffer all data before closing, never write after close"],"tags":["io","stream","encryption","lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}