googleapis/mcp-toolbox · error

failed to move %q to %q in bucket %q: %w

Error message

failed to move %q to %q in bucket %q: %w

What it means

MoveObject uses Cloud Storage's native ObjectHandle.Move API to atomically rename an object within a single bucket. This error wraps any failure of that call: the object is left untouched on failure (atomic). Notably, the Move API is not supported by all emulators/backends (e.g. fake-gcs-server), and it requires both read on the source and create/delete on the destination name.

Source

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

		"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
	}
	attrs, err := s.client.Bucket(bucket).Object(sourceObject).Move(ctx, storage.MoveObjectDestination{Object: destinationObject})
	if err != nil {
		return nil, fmt.Errorf("failed to move %q to %q in bucket %q: %w", sourceObject, destinationObject, bucket, err)
	}

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

// DeleteObject deletes a GCS object.
func (s *Source) DeleteObject(ctx context.Context, bucket, object string) (map[string]any, error) {
	if err := s.validateBucket(bucket); err != nil {
		return nil, err
	}
	if err := s.client.Bucket(bucket).Object(object).Delete(ctx); err != nil {
		return nil, fmt.Errorf("failed to delete object %q in bucket %q: %w", object, bucket, err)

View on GitHub (pinned to 8cc6e09de2)

Solutions

  1. Check errors.Is(err, storage.ErrObjectNotExist): verify the source object exists via ObjectHandle.Attrs before moving.
  2. If running against an emulator (fake-gcs-server etc.) that lacks the Move API, fall back to CopyObject followed by DeleteObject.
  3. Grant storage.objects.create and storage.objects.delete on the bucket to the credentials.
  4. Ensure the move is same-bucket; for cross-bucket moves use CopyObject then DeleteObject explicitly.
  5. Retry on 429/5xx with backoff; the atomic move makes retries safe, but handle 'already moved' by checking destination attrs.

Example fix

// before: unconditional native move
attrs, err := obj.Move(ctx, storage.MoveObjectDestination{Object: destinationObject})
// after: fallback when the backend doesn't support Move
if _, aerr := obj.Attrs(ctx); aerr != nil {
    return nil, fmt.Errorf("source %q not found: %w", sourceObject, aerr)
}
attrs, err := obj.Move(ctx, storage.MoveObjectDestination{Object: destinationObject})
if err != nil {
    var apiErr *googleapi.Error
    if errors.As(err, &apiErr) && apiErr.Code == http.StatusNotImplemented {
        return s.copyThenDelete(ctx, bucket, sourceObject, destinationObject)
    }
    return nil, fmt.Errorf("failed to move %q to %q in bucket %q: %w", sourceObject, destinationObject, bucket, err)
}
Defensive patterns

Strategy: fallback

Validate before calling

// Go: confirm source exists and destination is free before MoveObject
func precheckMove(ctx context.Context, src *cloudstorage.Source, bucket, sourceObject, destinationObject string) error {
    if _, err := src.Client.Bucket(bucket).Object(sourceObject).Attrs(ctx); err != nil {
        return fmt.Errorf("source gs://%s/%s missing: %w", bucket, sourceObject, err)
    }
    if _, err := src.Client.Bucket(bucket).Object(destinationObject).Attrs(ctx); err == nil {
        return fmt.Errorf("destination %q already exists", destinationObject)
    }
    return nil
}

Type guard

// Go: detect unsupported-Move backends (emulators)
func isMoveUnsupported(err error) bool {
    var apiErr *googleapi.Error
    return errors.As(err, &apiErr) && (apiErr.Code == 501 || apiErr.Code == 400)
}

Try / catch

_, err := src.MoveObject(ctx, bucket, sourceObject, destinationObject)
if err != nil {
    if isMoveUnsupported(err) {
        // fallback: copy then delete (emulators / older API versions)
        if _, cerr := src.CopyObject(ctx, bucket, sourceObject, bucket, destinationObject); cerr != nil {
            return fmt.Errorf("move fallback copy failed: %w", cerr)
        }
        if _, derr := src.DeleteObject(ctx, bucket, sourceObject); derr != nil {
            return fmt.Errorf("move fallback delete failed: %w", derr)
        }
        return nil
    }
    return fmt.Errorf("move failed: %w", err)
}

Prevention

When it happens

Trigger: Calling MoveObject(bucket, sourceObject, destinationObject) when: the source object does not exist, the destination name already exists (depending on API semantics/preconditions) or is invalid, the caller lacks storage.objects.delete/create permissions, the bucket uses a backend without Move support (emulators, older API versions), or ctx is cancelled.

Common situations: Test suites using fake-gcs-server or the GCS emulator that don't implement the move API, pipelines racing on rename where the source was already moved, IAM roles granting create but not delete, cross-bucket moves mistakenly passed to MoveObject instead of CopyObject+Delete.

Related errors


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