{"record":{"id":"0e9738c155789aff","repo":"apache/flink","slug":"cannot-commit-empty-multipart-upload-for-object","errorCode":null,"errorMessage":"Cannot commit empty multipart upload for object: {}. This indicates a programming error - at least one part must be uploaded before committing.","messagePattern":"Cannot commit empty multipart upload for object: (.+?)\\. This indicates a programming error - at least one part must be uploaded before committing\\.","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3Committer.java","lineNumber":66,"sourceCode":"            NativeS3ObjectOperations s3AccessHelper, NativeS3Recoverable recoverable) {\n        this.s3AccessHelper = s3AccessHelper;\n        this.recoverable = recoverable;\n    }\n\n    /**\n     * Commits the multipart upload to finalize the S3 object.\n     *\n     * <p><b>Empty Parts Check:</b> Attempting to commit with no parts is considered a programming\n     * error and will throw an IOException. This should not happen in normal operation as at least\n     * one part must be uploaded before committing. If this exception is thrown, it indicates a bug\n     * in the calling code or corruption of the recoverable state.\n     *\n     * @throws IOException if the commit fails or if attempting to commit with no parts\n     */\n    @Override\n    public void commit() throws IOException {\n        if (recoverable.parts().isEmpty()) {\n            throw new IOException(\n                    \"Cannot commit empty multipart upload for object: \"\n                            + recoverable.getObjectName()\n                            + \". This indicates a programming error - at least one part \"\n                            + \"must be uploaded before committing.\");\n        }\n\n        s3AccessHelper.commitMultiPartUpload(\n                recoverable.getObjectName(),\n                recoverable.uploadId(),\n                recoverable.parts().stream()\n                        .map(\n                                part ->\n                                        new NativeS3ObjectOperations.UploadPartResult(\n                                                part.getPartNumber(), part.getETag()))\n                        .collect(Collectors.toList()),\n                recoverable.numBytesInParts());\n    }\n","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/apache/flink/blob/2f3c205e9266cb30240eb7f4fdab15cad629a70f/flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3Committer.java#L48-L84","documentation":"NativeS3Committer.commit() calls the S3 CompleteMultipartUpload API, which S3 rejects (and the AWS SDK models poorly) when the part list is empty. The code treats recoverable.parts().isEmpty() as a contract violation and throws IOException with 'programming error' wording: at least one part (even a zero-byte part for an empty file) must have been uploaded and recorded in the recoverable state before commit. Its javadoc explicitly says this signals a bug in the calling code or corruption of the persisted recoverable state.","triggerScenarios":"persist() captured after open() but before any data flush/part upload, then that recoverable state handed to commit(); a truncated/corrupted recoverable state file where the parts list deserialized as empty; a custom RecoverableWriter implementation or test harness that constructs NativeS3Recoverable with an empty parts list; edge case where a part-upload succeeded but the state persisted before recording it.","commonSituations":"Writing an empty file with a buggy flush/persist ordering; checkpointing a stream between open and first part; recovering a committable from an older savepoint whose parts field was not serialized; unit tests constructing recoverables by hand.","solutions":["Ensure persist()/snapshot is only taken after at least one part exists: for an empty object, upload a single zero-byte part (the writer normally does this in flushForPersist) before persisting recoverable state.","If writing empty files is legitimate, verify the writer emitted the empty-part marker; if it did not, upgrade/patch the writer rather than catching this exception — it indicates no object will be committed.","Audit custom code that builds NativeS3Recoverable / calls commit() directly: pass the parts returned by uploadPart calls recorded in recoverable state.","If the recoverable state came from persistent storage and deserialized empty, treat the state as corrupt: discard the committable and re-run the write, and investigate why the parts list was lost."],"exampleFix":"// before — persisting before any part exists\nNativeS3Recoverable rec = writer.persistAfterRecovery(); // or captured pre-flush\nnew NativeS3Committer(rec, helper).commit(); // throws: empty parts\n\n// after — flush so at least one part (possibly zero-byte) is uploaded, then persist\nout.flush(); // uploads/pads the current part\nNativeS3Recoverable rec = ((NativeS3RecoverableDataOutputStream) out).persist();\nnew NativeS3Committer(rec, helper).commit();","handlingStrategy":"validation","validationCode":"// Never hand a possibly-empty recoverable to the committer\nif (recoverable.parts().isEmpty()) {\n    throw new IllegalStateException(\n        \"Refusing to commit with zero parts for \" + recoverable.getObjectName()\n        + \" — flush the stream so at least one part exists before persisting\");\n}","typeGuard":"boolean isCommittable(NativeS3Recoverable r) { return r != null && r.parts() != null && !r.parts().isEmpty() && r.uploadId() != null; }","tryCatchPattern":"catch (IOException e) { if (e.getMessage()!=null && e.getMessage().contains(\"empty multipart upload\")) { // programming error: do NOT retry; fix persist ordering / discard committable and re-run write } else throw e; }","preventionTips":["Only persist recoverable state after the writer flushed at least one part (empty files produce a zero-byte part).","Never construct NativeS3Recoverable by hand in user code or tests; use the RecoverableWriter's persist().","Treat this error as a bug report about the calling code, not an operational failure."],"tags":["multipart-upload","writer","recoverable-state","programming-error","s3","flink"],"backgroundTag":null,"analyzedSha":"2f3c205e9266cb30240eb7f4fdab15cad629a70f","analyzedAt":"2026-08-14T08:48:24.518Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}