googleapis/mcp-toolbox · error
failed to finalize write to
Error message
failed to finalize write to %q/%q: %w
What it means
WriteObject finalizes the GCS object in w.Close(); the actual upload commit and any server-side validation happen there. This error wraps a non-nil Close return, meaning the object write was rejected or failed at finalize and no object was created. The wrapped error carries the authoritative cause (permissions, quota, cancellation, 5xx).
Solutions
- Grant storage.objects.create (plus storage.objects.delete to overwrite) to the service account on the bucket.
- Unwrap and classify with errors.Is/errors.As: retry only on 429/5xx or transient network errors with a fresh context.
- Verify the bucket still exists and the name/region is correct via BucketHandle.Attrs before writing.
- Check org policies (CMEK enforcement, retention/soft-delete) that reject the finalize request.
- If finalize times out on large payloads, stream from a file with UploadObject instead of an in-memory string write.
Example fix
// before
if err := w.Close(); err != nil {
return nil, fmt.Errorf("failed to finalize write to %q/%q: %w", bucket, object, err)
}
// after: caller-side retry for transient finalize failures
if err := w.Close(); err != nil {
var apiErr *googleapi.Error
if errors.As(err, &apiErr) && (apiErr.Code == 429 || apiErr.Code >= 500) && attempt < 3 {
return retryWriteObject(ctx, bucket, object, content, contentType, attempt+1)
}
return nil, fmt.Errorf("failed to finalize write to %q/%q: %w", bucket, object, err)
} Defensive patterns
Strategy: retry
Validate before calling
// Go: confirm write permission before finalizing uploads
func canCreateObjects(ctx context.Context, client *storage.Client, bucket string) error {
probe := client.Bucket(bucket).Object(".toolbox-permission-probe").NewWriter(ctx)
probe.ContentType = "text/plain"
if _, err := io.WriteString(probe, "probe"); err != nil {
_ = probe.Close()
return fmt.Errorf("write probe failed: %w", err)
}
if err := probe.Close(); err != nil {
return fmt.Errorf("finalize probe failed (check storage.objects.create): %w", err)
}
return nil
} Type guard
// Go: narrow the wrapped error to googleapi.Error
func asGCSAPIError(err error) (*googleapi.Error, bool) {
var apiErr *googleapi.Error
ok := errors.As(err, &apiErr)
return apiErr, ok
} Try / catch
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
_, err := src.WriteObject(ctx, bucket, object, content, contentType)
if err == nil {
break
}
lastErr = err
if apiErr, ok := asGCSAPIError(err); !ok || (apiErr.Code != 429 && apiErr.Code < 500) {
return fmt.Errorf("non-retryable finalize failure: %w", err)
}
time.Sleep(time.Duration(1<<attempt) * time.Second)
}
return lastErr Prevention
- Add a startup permission probe (tiny write + Close) so IAM problems surface at boot, not mid-request.
- Retry only 429/5xx; treat 403/404 as configuration bugs.
- Monitor org policies (CMEK, retention) when provisioning destination buckets.
- Keep contexts alive through Close: never cancel the ctx between write and finalize.
- Check quota/billing alerts on the project — finalize failures often follow quota exhaustion.
When it happens
Trigger: Calling WriteObject(bucket, object, content, contentType) where io.WriteString succeeds but w.Close() fails: missing storage.objects.create permission, ctx cancelled during finalize, bucket deleted/renamed, CMEK/retention-policy rejection, or transient 5xx during the resumable-upload commit.
Common situations: Deployed workload with stale IAM after bucket policy changes, org-policy-enforced CMEK buckets receiving non-encrypted writes, request timeout hit exactly at finalize of a large payload, soft-delete/retention policies blocking overwrite of existing objects.
Related errors
- failed to finalize upload of
- failed to write content to object
- failed to copy / to /
- failed to move to in bucket
- allowedDataset ' ' not found in project
AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05).
Data as JSON: /api/errors/19f60ab1fc124418.
Report an issue: GitHub.
Appendix: source
Thrown at internal/sources/cloudstorage/cloudstorage.go:525
// 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
// from w.Attrs(), i.e. what GCS actually recorded.
func (s *Source) WriteObject(ctx context.Context, bucket, object, content, contentType string) (map[string]any, error) {
if err := s.validateBucket(bucket); err != nil {
return nil, err
}
w := s.client.Bucket(bucket).Object(object).NewWriter(ctx)
if contentType != "" {
w.ContentType = contentType
}
n, err := io.WriteString(w, content)
if err != nil {
_ = w.Close()
return nil, fmt.Errorf("failed to write content to object %q in bucket %q: %w", object, bucket, err)
}
if err := w.Close(); err != nil {
return nil, fmt.Errorf("failed to finalize write to %q/%q: %w", 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
}
// CopyObject copies an object to a destination object. The destination may be
// in the same bucket or a different bucket. Existing destination objects are
// replaced, matching Cloud Storage's copy semantics without preconditions.View on GitHub (pinned to 8cc6e09de2)