{"record":{"id":"fdd5c9644a286904","repo":"duplicati/duplicati","slug":"upload-succeeded-prematurely-uploaded-0-total","errorCode":null,"errorMessage":"Upload succeeded prematurely. Uploaded: {0}, total size: {1}","messagePattern":"Upload succeeded prematurely\\. Uploaded: (.+?), total size: (.+?)","errorType":"http","errorClass":"HttpRequestException","httpStatus":null,"severity":"error","filePath":"Duplicati/Library/Backend/GoogleServices/GoogleCommon.cs","lineNumber":189,"sourceCode":"                    req.Content.Headers.ContentType = new MediaTypeHeaderValue(\"application/octet-stream\");\r\n                    req.Content.Headers.Add(\"Content-Range\", $\"bytes {offset}-{offset + chunkSize - 1}/{stream.Length}\");\r\n                    using var resp = await oauth.GetResponseUncheckedAsync(req, HttpCompletionOption.ResponseContentRead, cancelToken).ConfigureAwait(false);\r\n\r\n                    // Check the response\r\n                    var code = (int)resp.StatusCode;\r\n\r\n                    if ((int)resp.StatusCode == 308 &&\r\n                        resp.Headers.TryGetValues(\"Range\", out var rangeValues) &&\r\n                        !string.IsNullOrWhiteSpace(rangeValues.FirstOrDefault()))\r\n                    {\r\n                        offset = long.Parse(rangeValues.First().Split('-')[1]) + 1;\r\n                        retries = 0;\r\n                    }\r\n                    else if (code >= 200 && code <= 299)\r\n                    {\r\n                        offset += chunkSize;\r\n                        if (offset != stream.Length)\r\n                            throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format(\"Upload succeeded prematurely. Uploaded: {0}, total size: {1}\", offset, stream.Length), null, resp.StatusCode);\r\n\r\n                        //Verify that the response is also valid (no timeout guard, as we already read the content)\r\n                        return await resp.Content.ReadFromJsonAsync<T>(cancelToken).ConfigureAwait(false)\r\n                            ?? throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format(\"Upload succeeded, but no data was returned, status code: {0}\", code), null, resp.StatusCode);\r\n                    }\r\n                    else\r\n                    {\r\n                        throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format(\"Unexpected status code: {0}\", code), null, resp.StatusCode);\r\n                    }\r\n                }\r\n                catch (Exception ex)\r\n                {\r\n                    var retry = false;\r\n\r\n                    // Check for HttpRequestException and inspect status code if available\r\n                    if (ex is HttpRequestException httpEx && httpEx.StatusCode.HasValue)\r\n                    {\r\n                        var code = (int)httpEx.StatusCode.Value;\r","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/duplicati/duplicati/blob/3f348be3e33f5d72d414e3ad55839c2ba34dda67/Duplicati/Library/Backend/GoogleServices/GoogleCommon.cs#L171-L207","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","On retry, query the server for the true offset (QueryUploadRange) before continuing instead of trusting local offset.","Ensure the source stream length is stable for the whole upload (do not append/truncate concurrently).","Check Content-Range header construction for off-by-one errors."],"exampleFix":"// before\noffset += chunkSize;\nif (offset != stream.Length)\n    throw new HttpRequestException(HttpRequestError.HttpProtocolError, string.Format(\"Upload succeeded prematurely. Uploaded: {0}, total size: {1}\", offset, stream.Length), null, resp.StatusCode);\n\n// after (treat object-complete response as authoritative when size matches server report)\noffset += chunkSize;\nif (offset != stream.Length)\n{\n    // Server says the object is complete. Trust it only if the response reports the full size.\n    var body = await resp.Content.ReadAsStringAsync(cancelToken).ConfigureAwait(false);\n    throw new HttpRequestException(HttpRequestError.HttpProtocolError,\n        $\"Upload reported complete at offset {offset} but stream length is {stream.Length}. Response: {body}\",\n        null, resp.StatusCode);\n}","handlingStrategy":"validation","validationCode":"// Before continuing a retry, query the server's true offset instead of trusting local state\nvar (serverOffset, _) = await QueryUploadRangeAsync(uploadUri, stream.Length);\nif (serverOffset == stream.Length) { /* already complete */ return; }","typeGuard":null,"tryCatchPattern":"try { await ChunkedUploadAsync<T>(...); }\ncatch (HttpRequestException ex) when (ex.Message.Contains(\"Upload succeeded prematurely\"))\n{\n    if (await ObjectSizeMatchesAsync(bucket, key, stream.Length, ct)) return; // server was right\n    throw;\n}","preventionTips":["On retry, always re-query the server offset rather than resuming from a stale local offset.","Keep stream.Length stable for the duration of the upload.","Double-check Content-Range header construction for off-by-one errors."],"tags":["gcs","google-common","resumable-upload","offset-mismatch","http-protocol","upload"],"backgroundTag":null,"analyzedSha":"3f348be3e33f5d72d414e3ad55839c2ba34dda67","analyzedAt":"2026-08-13T16:48:27.008Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}