QMail Object Commit — Group 6, Code 79
Verifies a transfer is complete and correct, then atomically publishes it as the object's new current generation. This is the last step of an upload started with object_begin.
Quick reference
| Command Group | 6 |
| Command Code | 79 |
| Server function | cmd_qmail_object_commit in qmail_object_transfer.c |
| Wire structures | qmail_object_commit_req_t, qmail_object_commit_resp_t |
| Body layout | Preamble (48) + Common prefix (16) + Commit request (64) = 128 bytes request header |
| Transport | TCP only |
| Encryption | Required — AES-128 |
| Idempotent | Yes — keyed by (authenticated owner, transfer_id) |
Purpose
Once the client believes every byte has been uploaded (confirmed with object_transfer_status or simply by having sent every planned range), it calls object_commit. The server verifies complete byte coverage, that the declared total_size and hash_algorithm match what was accepted at begin, and computes the actual hash over every received byte — rejecting with ERROR_HASH_MISMATCH if it does not match the object_hash supplied here.
Only after all verification succeeds does the server atomically change the current-generation pointer to the target generation accepted at begin. Readers must always see either the complete old generation or the complete new generation, never a mixture — a reader that already has an object_info response pinned to the old generation continues to see it consistently even while a commit is in progress elsewhere.
Request body
Preamble (48 bytes, offsets 0–47)
Standard QMail preamble. See QMail Overview — Universal preamble.
Common prefix (16 bytes, offsets 48–63)
See object_begin for the shared common-prefix field table. command_header_length must equal 64 for this command.
Commit request payload (64 bytes, offsets 64–127)
| Offset | Size | Field | Description |
|---|---|---|---|
| 64–79 | 16 | transfer_id | Must match the transfer_id returned by object_begin for this transfer. |
| 80–87 | 8 | total_size | Unsigned big-endian. Must match the value accepted at begin. |
| 88 | 1 | hash_algorithm | Must match the value accepted at begin (1 = SHA-256). |
| 89–95 | 7 | reserved | Must be zero. |
| 96–127 | 32 | object_hash | SHA-256 the server verifies against the actual received bytes before publishing. |
| last 2 | 2 | Terminator | Fixed clear-text terminator. |
Response body
Successful response header length: 96 bytes (16-byte common prefix + 80-byte commit response). No data follows.
| Field | Size | Description |
|---|---|---|
| common_prefix | 16 | Echoes request_id. |
| object_id | 16 | The object's stable identifier, as declared at begin. |
| file_type | 1 | Echo of the file_type declared at begin. |
| object_state | 1 | 1 = committed. |
| storage_class | 2 | The class the object was actually stored in. |
| generation | 8 | The generation just published — equal to the target_generation accepted at begin. |
| total_size | 8 | Echo of the committed size. |
| hash_algorithm | 1 | Echo of the request's hash_algorithm. |
| reserved | 3 | Zero. |
| object_hash | 32 | Echo of the verified object hash. |
| committed_at | 8 | Unix seconds when the generation was published. |
Status codes
| Code | Symbol | Meaning |
|---|---|---|
| 250 | SUCCESS | Object published; see response body. |
| 167 | ERROR_PAYMENT_PROCESSING | Payment for this transfer's owner/object_id/locker_code key is still pending. |
| 169 | ERROR_PAYMENT_REQUIRED | Payment state is failed; commit cannot proceed. |
| 218 | ERROR_TCP_REQUIRED | Retry using TCP. |
| 219 | ERROR_UNSUPPORTED_PROTOCOL | Unsupported protocol_version, framing version, nonzero flags, or hash_algorithm. |
| 222 | ERROR_TRANSFER_NOT_FOUND | transfer_id is unknown for this authenticated owner. |
| 223 | ERROR_TRANSFER_EXPIRED | The transfer existed but has expired. |
| 225 | ERROR_TRANSFER_INCOMPLETE | Commit attempted with missing byte ranges. |
| 226 | ERROR_HASH_MISMATCH | The complete-object hash computed by the server does not match the supplied object_hash. |
| 231 | ERROR_OBJECT_STATE | The transfer is not in a committable state (already committed with different fields, or aborted). |
| 234 | ERROR_TRANSFER_CONFLICT | A retry of commit supplied different total_size, hash_algorithm, or object_hash than the original successful commit. |
Idempotency
Commit is idempotent by owner and Transfer ID. If the commit response is lost in transit, an exact retry — identical total_size, hash_algorithm, and object_hash — returns the original successful commit's metadata rather than re-publishing or re-verifying. A retry with any different value returns ERROR_TRANSFER_CONFLICT, since that would mean the client is trying to commit a different object under the same Transfer ID.
Common mistakes
Committing before every range is uploaded
Calling commit while gaps remain returns ERROR_TRANSFER_INCOMPLETE, not a partial success. Use object_transfer_status with range_mode=0 to confirm zero missing ranges before committing, especially after resuming a dropped connection.
Computing object_hash over the wrong bytes
object_hash must be the hash of the exact bytes stored on this server — one stripe — not the sender's original, pre-striped file. Hashing the wrong data returns ERROR_HASH_MISMATCH even though every byte uploaded correctly.