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 valueView on GitHub (pinned to 8cc6e09de2)
Solutions
- Grant the credentials' service account storage.objects.create (and storage.objects.delete for overwrites) IAM role on the target bucket.
- Inspect the wrapped %w error: use errors.Is(err, context.Canceled/DeadlineExceeded) to detect ctx timeout and retry with a fresh, longer-lived context.
- Verify the bucket name is correct and the bucket still exists (gsutil ls / storage.BucketHandle.Attrs) before uploading.
- Check project billing/quota status and org policies (CMEK, retention) that can reject finalize.
- 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
- Grant storage.objects.create/delete to the workload's service account and verify with a smoke-test write at deploy time.
- Always pass a context with an explicit timeout sized to the file size.
- Check bucket existence/attrs before uploads in long-running processes (buckets can be deleted between runs).
- Use errors.Is/errors.As on the wrapped error instead of string matching.
- Test uploads against the real bucket (or emulator with matching IAM semantics) in CI.
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
- failed to write content to object %q in bucket %q: %w
- failed to finalize write to %q/%q: %w
- failed to verify allowedDataset '%s' in project '%s': %w
- failed to list objects in bucket %q: %w
- failed to open object %q in bucket %q: %w
AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05).
Data as JSON: /api/errors/79623ba5a02f34c6.
Report an issue: GitHub.