QMail Object Put Range — Group 6, Code 77
Uploads one byte range of a stored object opened by object_begin. Ranges may arrive concurrently, out of order, and be retried; the server tracks received coverage and reports how many new bytes each call actually added.
Quick reference
| Command Group | 6 |
| Command Code | 77 |
| Server function | cmd_qmail_object_put_range in qmail_object_transfer.c |
| Wire structures | qmail_object_put_range_req_t, qmail_object_put_range_resp_t |
| Body layout | Preamble (48) + Common prefix (16) + Range header (64) + data (N) |
| Transport | TCP only |
| Encryption | Required — AES-128 |
| Idempotent | Yes, for byte-identical retries of the same range |
Purpose
After object_begin returns an accepted_chunk size, the client uploads the object's bytes in one or more object_put_range calls. Ranges do not need to arrive in order and may be sent concurrently up to the max_parallel limit returned by begin. Each call carries a hash of just that range (range_hash) so the server can verify integrity before accepting the bytes, independent of the whole-object hash verified later at commit.
The response's received_unique field reports how many new bytes this call actually contributed to the transfer's coverage — not the size of the range in the request. A byte-identical resend of an already-received range reports 0 new bytes and still returns success.
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.
Range header (64 bytes fixed, offsets 64–127) + data
| Offset | Size | Field | Description |
|---|---|---|---|
| 64–79 | 16 | transfer_id | Must match the transfer_id returned by object_begin for this transfer. |
| 80–87 | 8 | offset | Unsigned big-endian byte offset of this range within the stored object. Must satisfy offset <= total_size. |
| 88–91 | 4 | data_length | Unsigned big-endian. Must equal the number of trailing data bytes actually present, and length <= total_size - offset. |
| 92 | 1 | hash_algorithm | 1 = SHA-256, matching the algorithm declared at begin. |
| 93–95 | 3 | reserved | Must be zero. |
| 96–127 | 32 | range_hash | Hash of just the data bytes that follow, verified before the range is accepted. |
| 128… | N | data | Raw range bytes, length data_length. |
| last 2 | 2 | Terminator | Fixed clear-text terminator. |
Response body
Successful response header length: 64 bytes (16-byte common prefix + 48-byte put_range response). No data follows.
| Field | Size | Description |
|---|---|---|
| common_prefix | 16 | Echoes request_id. |
| transfer_id | 16 | Echo of the request's transfer_id. |
| offset | 8 | Echo of the request's offset. |
| data_length | 4 | Echo of the request's data_length. |
| range_flags | 4 | Bit 0 = duplicate retry (this exact range was already received). All other bits zero in v1. |
| received_unique | 8 | Number of bytes this call newly contributed to the transfer's received coverage. May be less than data_length on a partial-overlap retry, or 0 on a full duplicate. |
| reserved | 8 | Zero. |
Status codes
| Code | Symbol | Meaning |
|---|---|---|
| 250 | SUCCESS | Range accepted (or was already fully received — see range_flags). |
| 218 | ERROR_TCP_REQUIRED | Retry using TCP. |
| 219 | ERROR_UNSUPPORTED_PROTOCOL | Unsupported protocol_version, framing version, nonzero flags, or hash_algorithm. |
| 221 | ERROR_RANGE_TOO_LARGE | data_length exceeds the accepted_chunk size or configured maximum. |
| 222 | ERROR_TRANSFER_NOT_FOUND | transfer_id is unknown for this authenticated owner. |
| 223 | ERROR_TRANSFER_EXPIRED | The transfer existed but has expired. |
| 224 | ERROR_RANGE_CONFLICT | This range overlaps bytes already received with different content. |
| 226 | ERROR_HASH_MISMATCH | range_hash does not match the received data bytes. |
| 229 | ERROR_INVALID_RANGE | offset, length, or arithmetic (offset + length > total_size) is invalid. |
| 231 | ERROR_OBJECT_STATE | The transfer is not in a state that accepts ranges (e.g. already committed or aborted). |
Overlap and idempotency
The server compares any overlap between a new range and bytes it has already received for this transfer. An overlap where every byte matches is accepted idempotently — the response's received_unique counts only the genuinely new portion, and range_flags bit 0 is set if the entire range was already present. An overlap where any byte differs from what was previously received returns ERROR_RANGE_CONFLICT; the client must not silently overwrite — it should either resend the exact original bytes or abort and restart the transfer with a new Transfer ID.
Received-range coverage is stored server-side in a sparse structure (not one bit per byte), so ranges may arrive in any order and be checked for completeness cheaply at object_transfer_status or object_commit time.
Common mistakes
Sending ranges larger than accepted_chunk
The chunk size negotiated at object_begin is a ceiling, not a suggestion. A range larger than accepted_chunk returns ERROR_RANGE_TOO_LARGE. Split larger uploads into multiple put_range calls.
Assuming received_unique equals data_length
On a fresh, non-overlapping range they are equal, but after a resume or partial retry they will not be. Use received_unique to track true upload progress, and use object_transfer_status, not a running sum of data_length, as the authoritative coverage check before commit.