{"record":{"id":"19f60ab1fc124418","repo":"googleapis/mcp-toolbox","slug":"failed-to-finalize-write-to-q-q-w","errorCode":null,"errorMessage":"failed to finalize write to %q/%q: %w","messagePattern":"failed to finalize write to %q/%q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sources/cloudstorage/cloudstorage.go","lineNumber":525,"sourceCode":"// is empty, the writer's ContentType is left unset so Cloud Storage detects it\n// from the first 512 bytes. The returned contentType is the post-Close value\n// from w.Attrs(), i.e. what GCS actually recorded.\nfunc (s *Source) WriteObject(ctx context.Context, bucket, object, content, contentType string) (map[string]any, error) {\n\tif err := s.validateBucket(bucket); err != nil {\n\t\treturn nil, err\n\t}\n\tw := s.client.Bucket(bucket).Object(object).NewWriter(ctx)\n\tif contentType != \"\" {\n\t\tw.ContentType = contentType\n\t}\n\n\tn, err := io.WriteString(w, content)\n\tif err != nil {\n\t\t_ = w.Close()\n\t\treturn nil, fmt.Errorf(\"failed to write content to object %q in bucket %q: %w\", object, bucket, err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to finalize write to %q/%q: %w\", bucket, object, err)\n\t}\n\n\tattrs := w.Attrs()\n\tfinalContentType := \"\"\n\tif attrs != nil {\n\t\tfinalContentType = attrs.ContentType\n\t}\n\treturn map[string]any{\n\t\t\"bucket\":      bucket,\n\t\t\"object\":      object,\n\t\t\"bytes\":       n,\n\t\t\"contentType\": finalContentType,\n\t}, nil\n}\n\n// CopyObject copies an object to a destination object. The destination may be\n// in the same bucket or a different bucket. Existing destination objects are\n// replaced, matching Cloud Storage's copy semantics without preconditions.","sourceCodeStart":507,"sourceCodeEnd":543,"githubUrl":"https://github.com/googleapis/mcp-toolbox/blob/8cc6e09de2ad7b8bffc77751799585a1401a48eb/internal/sources/cloudstorage/cloudstorage.go#L507-L543","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif err := w.Close(); err != nil {\n    return nil, fmt.Errorf(\"failed to finalize write to %q/%q: %w\", bucket, object, err)\n}\n// after: caller-side retry for transient finalize failures\nif err := w.Close(); err != nil {\n    var apiErr *googleapi.Error\n    if errors.As(err, &apiErr) && (apiErr.Code == 429 || apiErr.Code >= 500) && attempt < 3 {\n        return retryWriteObject(ctx, bucket, object, content, contentType, attempt+1)\n    }\n    return nil, fmt.Errorf(\"failed to finalize write to %q/%q: %w\", bucket, object, err)\n}","handlingStrategy":"retry","validationCode":"// Go: confirm write permission before finalizing uploads\nfunc canCreateObjects(ctx context.Context, client *storage.Client, bucket string) error {\n    probe := client.Bucket(bucket).Object(\".toolbox-permission-probe\").NewWriter(ctx)\n    probe.ContentType = \"text/plain\"\n    if _, err := io.WriteString(probe, \"probe\"); err != nil {\n        _ = probe.Close()\n        return fmt.Errorf(\"write probe failed: %w\", err)\n    }\n    if err := probe.Close(); err != nil {\n        return fmt.Errorf(\"finalize probe failed (check storage.objects.create): %w\", err)\n    }\n    return nil\n}","typeGuard":"// Go: narrow the wrapped error to googleapi.Error\nfunc asGCSAPIError(err error) (*googleapi.Error, bool) {\n    var apiErr *googleapi.Error\n    ok := errors.As(err, &apiErr)\n    return apiErr, ok\n}","tryCatchPattern":"var lastErr error\nfor attempt := 0; attempt < 3; attempt++ {\n    _, err := src.WriteObject(ctx, bucket, object, content, contentType)\n    if err == nil {\n        break\n    }\n    lastErr = err\n    if apiErr, ok := asGCSAPIError(err); !ok || (apiErr.Code != 429 && apiErr.Code < 500) {\n        return fmt.Errorf(\"non-retryable finalize failure: %w\", err)\n    }\n    time.Sleep(time.Duration(1<<attempt) * time.Second)\n}\nreturn lastErr","preventionTips":["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."],"tags":["gcs","write","permissions","context-cancelled"],"backgroundTag":"gcs-object-write-failed","analyzedSha":"8cc6e09de2ad7b8bffc77751799585a1401a48eb","analyzedAt":"2026-09-05T01:10:36.887Z","contentChangedAt":"2026-09-05T01:10:36.887Z","schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}