duplicati/duplicati · error · HttpRequestException

Upload succeeded prematurely. Uploaded: {0}, total size: {1}

Error message

Upload succeeded prematurely. Uploaded: {0}, total size: {1}

What it means

During ChunkedUploadAsync, when the server returns a 2xx (signalling upload complete) but the local offset plus chunkSize does not equal the total stream length, Duplicati throws HttpRequestException 'Upload succeeded prematurely' showing the bytes the server accepted vs the total size. The server declared completion before all bytes were sent, which violates the resumable-upload contract.

Source

Thrown at Duplicati/Library/Backend/GoogleServices/GoogleCommon.cs:189

                    req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
                    req.Content.Headers.Add("Content-Range", $"bytes {offset}-{offset + chunkSize - 1}/{stream.Length}");
                    using var resp = await oauth.GetResponseUncheckedAsync(req, HttpCompletionOption.ResponseContentRead, cancelToken).ConfigureAwait(false);

                    // Check the response
                    var code = (int)resp.StatusCode;

                    if ((int)resp.StatusCode == 308 &&
                        resp.Headers.TryGetValues("Range", out var rangeValues) &&
                        !string.IsNullOrWhiteSpace(rangeValues.FirstOrDefault()))
                    {
                        offset = long.Parse(rangeValues.First().Split('-')[1]) + 1;
                        retries = 0;
                    }
                    else if (code >= 200 && code <= 299)
                    {
                        offset += chunkSize;
                        if (offset != stream.Length)
                            throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format("Upload succeeded prematurely. Uploaded: {0}, total size: {1}", offset, stream.Length), null, resp.StatusCode);

                        //Verify that the response is also valid (no timeout guard, as we already read the content)
                        return await resp.Content.ReadFromJsonAsync<T>(cancelToken).ConfigureAwait(false)
                            ?? throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format("Upload succeeded, but no data was returned, status code: {0}", code), null, resp.StatusCode);
                    }
                    else
                    {
                        throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format("Unexpected status code: {0}", code), null, resp.StatusCode);
                    }
                }
                catch (Exception ex)
                {
                    var retry = false;

                    // Check for HttpRequestException and inspect status code if available
                    if (ex is HttpRequestException httpEx && httpEx.StatusCode.HasValue)
                    {
                        var code = (int)httpEx.StatusCode.Value;

View on GitHub (pinned to 3f348be3e3)

Solutions

  1. Verify the uploaded object size matches the local file (gsutil stat); if it does, the 'premature' completion was actually correct and the offset was stale.
  2. On retry, query the server for the true offset (QueryUploadRange) before continuing instead of trusting local offset.
  3. Ensure the source stream length is stable for the whole upload (do not append/truncate concurrently).
  4. Check Content-Range header construction for off-by-one errors.

Example fix

// before
offset += chunkSize;
if (offset != stream.Length)
    throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format("Upload succeeded prematurely. Uploaded: {0}, total size: {1}", offset, stream.Length), null, resp.StatusCode);

// after (treat object-complete response as authoritative when size matches server report)
offset += chunkSize;
if (offset != stream.Length)
{
    // Server says the object is complete. Trust it only if the response reports the full size.
    var body = await resp.Content.ReadAsStringAsync(cancelToken).ConfigureAwait(false);
    throw new HttpRequestException(HttpRequestError.HttpProtocolError,
        $"Upload reported complete at offset {offset} but stream length is {stream.Length}. Response: {body}",
        null, resp.StatusCode);
}
Defensive patterns

Strategy: validation

Validate before calling

// Before continuing a retry, query the server's true offset instead of trusting local state
var (serverOffset, _) = await QueryUploadRangeAsync(uploadUri, stream.Length);
if (serverOffset == stream.Length) { /* already complete */ return; }

Try / catch

try { await ChunkedUploadAsync<T>(...); }
catch (HttpRequestException ex) when (ex.Message.Contains("Upload succeeded prematurely"))
{
    if (await ObjectSizeMatchesAsync(bucket, key, stream.Length, ct)) return; // server was right
    throw;
}

Prevention

When it happens

Trigger: A PUT chunk receives 2xx (final) while offset+chunkSize < stream.Length. The server reports the object complete despite the client believing more bytes remain — e.g. because an earlier retry already delivered the remaining bytes, or the Content-Range/offset accounting drifted.

Common situations: Retry after a partial failure where the server already has the full object (offset was stale); chunkSize/offset arithmetic bug; stream.Length changed mid-upload; Content-Range header mismatch.

Related errors


AI-assisted analysis of duplicati/duplicati@3f348be3e3 (2026-08-13). Data as JSON: /api/errors/fdd5c9644a286904. Report an issue: GitHub.