googleapis/mcp-toolbox · error

failed to copy %q/%q to %q/%q: %w

Error message

failed to copy %q/%q to %q/%q: %w

What it means

CopyObject runs dst.CopierFrom(src).Run(ctx), a single GCS copy API call; this error wraps any failure of that call. The copy is atomic server-side — either the destination object is created from the source or nothing changes. Because the wrapped error is the raw GCS API error, its storage.ErrObjectNotExist / googleapi.Error codes directly identify which side (source or destination) is at fault.

Source

Thrown at internal/sources/cloudstorage/cloudstorage.go:556

	}, 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.
func (s *Source) CopyObject(ctx context.Context, sourceBucket, sourceObject, destinationBucket, destinationObject string) (map[string]any, error) {
	if err := s.validateBucket(sourceBucket); err != nil {
		return nil, err
	}
	if err := s.validateBucket(destinationBucket); err != nil {
		return nil, err
	}
	src := s.client.Bucket(sourceBucket).Object(sourceObject)
	dst := s.client.Bucket(destinationBucket).Object(destinationObject)

	attrs, err := dst.CopierFrom(src).Run(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to copy %q/%q to %q/%q: %w", sourceBucket, sourceObject, destinationBucket, destinationObject, err)
	}

	return map[string]any{
		"sourceBucket":      sourceBucket,
		"sourceObject":      sourceObject,
		"destinationBucket": destinationBucket,
		"destinationObject": destinationObject,
		"bytes":             attrs.Size,
		"contentType":       attrs.ContentType,
	}, nil
}

// MoveObject atomically renames or moves an object within the same bucket using
// Cloud Storage's native move API. Cross-bucket moves should be modeled as
// CopyObject followed by DeleteObject.
func (s *Source) MoveObject(ctx context.Context, bucket, sourceObject, destinationObject string) (map[string]any, error) {
	if err := s.validateBucket(bucket); err != nil {
		return nil, err

View on GitHub (pinned to 8cc6e09de2)

Solutions

  1. Check errors.Is(err, storage.ErrObjectNotExist) to confirm a missing source object; verify the object path exists via ObjectHandle.Attrs before copying.
  2. Grant storage.objects.get on the source bucket and storage.objects.create on the destination bucket to the credentials.
  3. Verify both bucket names and that both buckets exist (BucketHandle.Attrs).
  4. If the wrapped googleapi.Error is 403 with a constraint/org-policy reason, align destination bucket policies (CMEK, location) with the source.
  5. On 429/5xx, retry the copy with backoff; CopyObject is safe to retry because the destination is overwritten atomically.

Example fix

// before: assume source exists
attrs, err := dst.CopierFrom(src).Run(ctx)
// after: pre-check source and classify errors
if _, err := s.client.Bucket(sourceBucket).Object(sourceObject).Attrs(ctx); err != nil {
    return nil, fmt.Errorf("source %q/%q not found: %w", sourceBucket, sourceObject, err)
}
attrs, err := dst.CopierFrom(src).Run(ctx)
if err != nil {
    var apiErr *googleapi.Error
    if errors.As(err, &apiErr) && apiErr.Code == 403 {
        // inspect permissions on destination bucket
    }
    return nil, fmt.Errorf("failed to copy %q/%q to %q/%q: %w", sourceBucket, sourceObject, destinationBucket, destinationObject, err)
}
Defensive patterns

Strategy: validation

Validate before calling

// Go: verify source object and both buckets before CopyObject
func precheckCopy(ctx context.Context, src *cloudstorage.Source, sBucket, sObject, dBucket, dObject string) error {
    if _, err := src.Client.Bucket(sBucket).Object(sObject).Attrs(ctx); err != nil {
        return fmt.Errorf("source gs://%s/%s missing: %w", sBucket, sObject, err)
    }
    if _, err := src.Client.Bucket(dBucket).Attrs(ctx); err != nil {
        return fmt.Errorf("destination bucket %q missing: %w", dBucket, err)
    }
    return nil
}

Type guard

// Go: distinguish missing-object errors from permission errors
func copyFailureKind(err error) string {
    if errors.Is(err, storage.ErrObjectNotExist) {
        return "not-found"
    }
    var apiErr *googleapi.Error
    if errors.As(err, &apiErr) {
        if apiErr.Code == 403 || apiErr.Code == 401 {
            return "permission"
        }
        if apiErr.Code == 404 {
            return "bucket-not-found"
        }
    }
    return "other"
}

Try / catch

err := copyFailureKind(nil) // placeholder removal
_, err = src.CopyObject(ctx, sBucket, sObject, dBucket, dObject)
if err != nil {
    switch copyFailureKind(err) {
    case "not-found":
        return fmt.Errorf("source object gs://%s/%s does not exist", sBucket, sObject)
    case "permission", "bucket-not-found":
        return fmt.Errorf("check IAM/bucket config: %w", err)
    default:
        // transient: safe to retry, copy overwrites destination atomically
    }
}

Prevention

When it happens

Trigger: Calling CopyObject(sourceBucket, sourceObject, destinationBucket, destinationObject) when: the source object does not exist (storage.ErrObjectNotExist), the caller lacks storage.objects.get on source or storage.objects.create on destination, either bucket is absent/misnamed, the destination bucket is in a different location with conflicting constraints, or the copy exceeds limits / ctx is cancelled.

Common situations: Copying objects whose names contain unencoded special characters, moving data between buckets in different regions/projects with cross-project permission gaps, deleting-then-copying races in pipelines, typos in object paths, buckets rejected by org policy (CMEK, uniform bucket-level access).

Related errors


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