duplicati/duplicati · error · HttpRequestException

Failed to start upload session

Error message

Failed to start upload session

What it means

ChunkedUploadWithResumeAsync starts a resumable session by POSTing metadata; GCS must return a Location header with the upload URI. If the Location header is absent or empty, Duplicati throws HttpRequestException (Unknown error) 'Failed to start upload session' with the response StatusCode attached. A second, defensive Exception with the same message covers the unreachable case where the header existed but the returned value is still whitespace.

Source

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

        /// <param name="readWriteTimeout">The read write timeout.</param>
        /// <param name="method">The HTTP Method.</param>
        /// <typeparam name="TRequest">The type of data to upload as metadata.</typeparam>
        /// <typeparam name="TResponse">The type of data returned from the upload.</typeparam>
        public static async Task<TResponse> ChunkedUploadWithResumeAsync<TRequest, TResponse>(JsonWebHelperHttpClient oauth, TRequest requestdata, string url, Stream stream, TimeSpan shortTimeout, TimeSpan readWriteTimeout, CancellationToken cancelToken, HttpMethod method)
            where TRequest : class
            where TResponse : class
        {
            using var req = await oauth.CreateRequestAsync(url, method, cancelToken);
            if (requestdata != null)
                req.Content = JsonContent.Create(requestdata);
            req.Headers.Add("X-Upload-Content-Type", "application/octet-stream");
            req.Headers.Add("X-Upload-Content-Length", stream.Length.ToString());

            var uploaduri = await Utility.Utility.WithTimeout(shortTimeout, cancelToken, async ct =>
            {
                using var resp = await oauth.GetResponseAsync(req, HttpCompletionOption.ResponseContentRead, ct).ConfigureAwait(false);
                if (!resp.Headers.TryGetValues("Location", out var locationValues) || string.IsNullOrWhiteSpace(locationValues.FirstOrDefault()))
                    throw new HttpRequestException(HttpRequestError.Unknown, "Failed to start upload session", null, resp.StatusCode);

                return locationValues.First();
            }).ConfigureAwait(false);

            if (string.IsNullOrWhiteSpace(uploaduri))
                throw new Exception("Failed to start upload session");

            return await ChunkedUploadAsync<TResponse>(oauth, uploaduri, stream, shortTimeout, readWriteTimeout, cancelToken).ConfigureAwait(false);
        }

        /// <summary>
        /// Helper method that performs a chunked upload, and queries for http status after each chunk
        /// </summary>
        /// <returns>The response item</returns>
        /// <param name="oauth">The Oauth instance</param>
        /// <param name="uploaduri">The resumeable uploaduri</param>
        /// <param name="stream">The stream with data to upload.</param>
        /// <param name="shortTimeout">The short request timeout.</param>

View on GitHub (pinned to 3f348be3e3)

Solutions

  1. Confirm the bucket exists and the account has storage.objects.create on it.
  2. Capture the initiation response status/body — a non-2xx here usually carries the real error.
  3. Ensure no intermediary (proxy/SDK wrapper) strips the Location response header.
  4. Retry once for transient 5xx; if it persists, escalate with the captured status code.

Example fix

// before
if (!resp.Headers.TryGetValues("Location", out var locationValues) || string.IsNullOrWhiteSpace(locationValues.FirstOrDefault()))
    throw new HttpRequestException(HttpRequestError.Unknown, "Failed to start upload session", null, resp.StatusCode);

// after (include body + status for diagnosis)
if (!resp.Headers.TryGetValues("Location", out var locationValues) || string.IsNullOrWhiteSpace(locationValues.FirstOrDefault()))
{
    var body = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
    throw new HttpRequestException(HttpRequestError.Unknown,
        $"Failed to start upload session (status {(int)resp.StatusCode}): {body}",
        null, resp.StatusCode);
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify the account can create objects in the bucket before initiating
// storage.objects.get on a known key, or check IAM roles/storage.objectCreator

Try / catch

try { await GoogleCommon.ChunkedUploadWithResumeAsync<...>(...); }
catch (HttpRequestException ex) when (ex.Message.Contains("Failed to start upload session"))
{
    // status code carries the real cause (404 bucket, 403 perms, 5xx transient)
    _logger.LogError(ex, "Upload session init failed with status {Status}", ex.StatusCode);
    throw;
}

Prevention

When it happens

Trigger: The session-initiation POST returns 2xx but no Location header, or returns a non-2xx (the body of the if is only reached on the TryGetValues path). Common when auth/permission/quota prevents session creation but the response shape omits Location.

Common situations: Service account lacks permission to create objects in the bucket; bucket does not exist (so init 404s and no Location); resumable-upload feature disabled; a proxy strips the Location header; transient GCS error returning an empty body.

Related errors


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