{"record":{"id":"79623ba5a02f34c6","repo":"googleapis/mcp-toolbox","slug":"failed-to-finalize-upload-of-q-to-q-q-w","errorCode":null,"errorMessage":"failed to finalize upload of %q to %q/%q: %w","messagePattern":"failed to finalize upload of %q to %q/%q: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/sources/cloudstorage/cloudstorage.go","lineNumber":490,"sourceCode":"\t}\n\tdefer f.Close()\n\n\tif contentType == \"\" {\n\t\tcontentType = mime.TypeByExtension(filepath.Ext(source))\n\t}\n\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.Copy(w, f)\n\tif err != nil {\n\t\t_ = w.Close()\n\t\treturn nil, fmt.Errorf(\"failed to copy %q to object %q in bucket %q: %w\", source, object, bucket, err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to finalize upload of %q to %q/%q: %w\", source, 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// WriteObject writes text content directly into a GCS object. When contentType\n// 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","sourceCodeStart":472,"sourceCodeEnd":508,"githubUrl":"https://github.com/googleapis/mcp-toolbox/blob/8cc6e09de2ad7b8bffc77751799585a1401a48eb/internal/sources/cloudstorage/cloudstorage.go#L472-L508","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: opaque retry loop on any error\nif err := w.Close(); err != nil {\n    return fmt.Errorf(\"upload failed: %w\", err)\n}\n// after: classify before retrying\nif err := w.Close(); err != nil {\n    var apiErr *googleapi.Error\n    if errors.Is(err, context.DeadlineExceeded) || (errors.As(err, &apiErr) && (apiErr.Code == 429 || apiErr.Code >= 500)) {\n        return retryUpload(ctx, bucket, object, source, contentType)\n    }\n    return fmt.Errorf(\"failed to finalize upload of %q to %q/%q: %w\", source, bucket, object, err)\n}","handlingStrategy":"try-catch","validationCode":"// Go: verify bucket and source file before calling UploadObject\nfunc precheck(ctx context.Context, src *cloudstorage.Source, bucket, object, source string) error {\n    if _, err := os.Stat(source); err != nil {\n        return fmt.Errorf(\"source file missing: %w\", err)\n    }\n    if _, err := src.Client.Bucket(bucket).Attrs(ctx); err != nil {\n        return fmt.Errorf(\"bucket %q unavailable: %w\", bucket, err)\n    }\n    return nil\n}","typeGuard":"// Go: classify the wrapped GCS error\nfunc isTransientGCSError(err error) bool {\n    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {\n        return false\n    }\n    var apiErr *googleapi.Error\n    if errors.As(err, &apiErr) {\n        return apiErr.Code == 429 || apiErr.Code >= 500\n    }\n    return false\n}","tryCatchPattern":"result, err := src.UploadObject(ctx, bucket, object, filePath, contentType)\nif err != nil {\n    var apiErr *googleapi.Error\n    switch {\n    case errors.As(err, &apiErr) && apiErr.Code == 403:\n        // fix IAM: storage.objects.create on the bucket\n    case isTransientGCSError(err):\n        // retry with backoff and fresh context\n    default:\n        // log wrapped cause: fmt.Sprintf(\"%+v\", err)\n    }\n}","preventionTips":["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."],"tags":["gcs","upload","permissions","network","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"}