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

  1. Grant storage.objects.create (plus storage.objects.delete to overwrite) to the service account on the bucket.
  2. Unwrap and classify with errors.Is/errors.As: retry only on 429/5xx or transient network errors with a fresh context.
  3. Verify the bucket still exists and the name/region is correct via BucketHandle.Attrs before writing.
  4. Check org policies (CMEK enforcement, retention/soft-delete) that reject the finalize request.
  5. 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

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


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