googleapis/mcp-toolbox · error

failed to finalize upload of %q to %q/%q: %w

Error message

failed to finalize upload of %q to %q/%q: %w

What it means

UploadObject streams a local file into a GCS object via storage.Writer. All bytes may be buffered, but the upload only commits when w.Close() is called; this error wraps any failure returned by that final Close, meaning GCS rejected or failed to complete the object upload after the data was copied. The wrap preserves the underlying googleapi/storage error (permissions, quotas, context cancellation, preconditions).

Source

Thrown at internal/sources/cloudstorage/cloudstorage.go:490

	}
	defer f.Close()

	if contentType == "" {
		contentType = mime.TypeByExtension(filepath.Ext(source))
	}

	w := s.client.Bucket(bucket).Object(object).NewWriter(ctx)
	if contentType != "" {
		w.ContentType = contentType
	}

	n, err := io.Copy(w, f)
	if err != nil {
		_ = w.Close()
		return nil, fmt.Errorf("failed to copy %q to object %q in bucket %q: %w", source, object, bucket, err)
	}
	if err := w.Close(); err != nil {
		return nil, fmt.Errorf("failed to finalize upload of %q to %q/%q: %w", source, bucket, object, err)
	}

	attrs := w.Attrs()
	finalContentType := ""
	if attrs != nil {
		finalContentType = attrs.ContentType
	}
	return map[string]any{
		"bucket":      bucket,
		"object":      object,
		"bytes":       n,
		"contentType": finalContentType,
	}, nil
}

// WriteObject writes text content directly into a GCS object. When contentType
// is empty, the writer's ContentType is left unset so Cloud Storage detects it
// from the first 512 bytes. The returned contentType is the post-Close value

View on GitHub (pinned to 8cc6e09de2)

Solutions

  1. Grant the credentials' service account storage.objects.create (and storage.objects.delete for overwrites) IAM role on the target bucket.
  2. Inspect the wrapped %w error: use errors.Is(err, context.Canceled/DeadlineExceeded) to detect ctx timeout and retry with a fresh, longer-lived context.
  3. Verify the bucket name is correct and the bucket still exists (gsutil ls / storage.BucketHandle.Attrs) before uploading.
  4. Check project billing/quota status and org policies (CMEK, retention) that can reject finalize.
  5. Retry once on transient errors (storage.ErrObjectNotExist is not retryable; 5xx/429 are) — use googleapi.Error codes to decide.

Example fix

// before: opaque retry loop on any error
if err := w.Close(); err != nil {
    return fmt.Errorf("upload failed: %w", err)
}
// after: classify before retrying
if err := w.Close(); err != nil {
    var apiErr *googleapi.Error
    if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &apiErr) && (apiErr.Code == 429 || apiErr.Code >= 500)) {
        return retryUpload(ctx, bucket, object, source, contentType)
    }
    return fmt.Errorf("failed to finalize upload of %q to %q/%q: %w", source, bucket, object, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Go: verify bucket and source file before calling UploadObject
func precheck(ctx context.Context, src *cloudstorage.Source, bucket, object, source string) error {
    if _, err := os.Stat(source); err != nil {
        return fmt.Errorf("source file missing: %w", err)
    }
    if _, err := src.Client.Bucket(bucket).Attrs(ctx); err != nil {
        return fmt.Errorf("bucket %q unavailable: %w", bucket, err)
    }
    return nil
}

Type guard

// Go: classify the wrapped GCS error
func isTransientGCSError(err error) bool {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
        return false
    }
    var apiErr *googleapi.Error
    if errors.As(err, &apiErr) {
        return apiErr.Code == 429 || apiErr.Code >= 500
    }
    return false
}

Try / catch

result, err := src.UploadObject(ctx, bucket, object, filePath, contentType)
if err != nil {
    var apiErr *googleapi.Error
    switch {
    case errors.As(err, &apiErr) && apiErr.Code == 403:
        // fix IAM: storage.objects.create on the bucket
    case isTransientGCSError(err):
        // retry with backoff and fresh context
    default:
        // log wrapped cause: fmt.Sprintf("%+v", err)
    }
}

Prevention

When it happens

Trigger: Calling UploadObject(bucket, object, source, contentType) where io.Copy succeeds but w.Close() fails: service account lacks storage.objects.create on the bucket, ctx is cancelled mid-finalize, bucket is missing/deleted, or a transient HTTP/5xx error occurs while GCS finalizes the resumable upload.

Common situations: IAM changes after client setup (writer lost storage.objects.create), bucket removed or renamed between validateBucket and upload, request context deadlines exceeded on large files, org policies (e.g. CMEK-required buckets) rejecting the write at finalize time, quota/billing errors on the project.

Related errors


AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05). Data as JSON: /api/errors/79623ba5a02f34c6. Report an issue: GitHub.