googleapis/mcp-toolbox · error
failed to finalize write to %q/%q: %w
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).
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)
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 %q to %q/%q: %w
- failed to write content to object %q in bucket %q: %w
- failed to copy %q/%q to %q/%q: %w
- failed to move %q to %q in bucket %q: %w
- failed to rename file: %w
AI-assisted analysis of googleapis/mcp-toolbox@8cc6e09de2 (2026-09-05).
Data as JSON: /api/errors/19f60ab1fc124418.
Report an issue: GitHub.