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 Group6
Command Code77
Server functioncmd_qmail_object_put_range in qmail_object_transfer.c
Wire structuresqmail_object_put_range_req_t, qmail_object_put_range_resp_t
Body layoutPreamble (48) + Common prefix (16) + Range header (64) + data (N)
TransportTCP only
EncryptionRequired — AES-128
IdempotentYes, 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

transfer_id (16 bytes) 64 79 offset (8, BE) 80 87 data_length (4) 88 91 HA 92 rsv (3) 93 95 range_hash (32 bytes, SHA-256 of this range) 96 127 data (N bytes, N = data_length) 128
OffsetSizeFieldDescription
64–7916transfer_idMust match the transfer_id returned by object_begin for this transfer.
80–878offsetUnsigned big-endian byte offset of this range within the stored object. Must satisfy offset <= total_size.
88–914data_lengthUnsigned big-endian. Must equal the number of trailing data bytes actually present, and length <= total_size - offset.
921hash_algorithm1 = SHA-256, matching the algorithm declared at begin.
93–953reservedMust be zero.
96–12732range_hashHash of just the data bytes that follow, verified before the range is accepted.
128…NdataRaw range bytes, length data_length.
last 22TerminatorFixed clear-text terminator.

Response body

Successful response header length: 64 bytes (16-byte common prefix + 48-byte put_range response). No data follows.

FieldSizeDescription
common_prefix16Echoes request_id.
transfer_id16Echo of the request's transfer_id.
offset8Echo of the request's offset.
data_length4Echo of the request's data_length.
range_flags4Bit 0 = duplicate retry (this exact range was already received). All other bits zero in v1.
received_unique8Number 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.
reserved8Zero.

Status codes

CodeSymbolMeaning
250SUCCESSRange accepted (or was already fully received — see range_flags).
218ERROR_TCP_REQUIREDRetry using TCP.
219ERROR_UNSUPPORTED_PROTOCOLUnsupported protocol_version, framing version, nonzero flags, or hash_algorithm.
221ERROR_RANGE_TOO_LARGEdata_length exceeds the accepted_chunk size or configured maximum.
222ERROR_TRANSFER_NOT_FOUNDtransfer_id is unknown for this authenticated owner.
223ERROR_TRANSFER_EXPIREDThe transfer existed but has expired.
224ERROR_RANGE_CONFLICTThis range overlaps bytes already received with different content.
226ERROR_HASH_MISMATCHrange_hash does not match the received data bytes.
229ERROR_INVALID_RANGEoffset, length, or arithmetic (offset + length > total_size) is invalid.
231ERROR_OBJECT_STATEThe 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.