JuliusBrussee/caveman · error

objectstore: delete version

Error message

objectstore: delete version %q (%q): %w

What it means

deleteVersions, after finding a version to remove, calls RemoveObject with that VersionID; this error wraps a failure of that per-version removal and includes both the key and version ID. The operation aborts the remaining version deletions. The unusual two-%q format ("delete version %q (%q)") reflects key + versionID.

Solutions

  1. Check the wrapped minio.ErrorResponse: AccessDenied means IAM needs s3:DeleteObjectVersion.
  2. Look for Object Lock retention/legal holds on the reported version and lift them or wait out retention.
  3. Make the loop resilient: collect failures per version and continue instead of aborting on first error.
  4. Retry transient 5xx/SlowDown errors with backoff.
  5. Verify backend support for versioned RemoveObject if using non-AWS S3-compatible storage.

Example fix

// before
for obj := range versions {
    if err := remove(ctx, obj); err != nil {
        return err // aborts on first held version
    }
}
// after
var errs []error
for obj := range versions {
    if err := remove(ctx, obj); err != nil {
        errs = append(errs, fmt.Errorf("%s@%s: %w", obj.Key, obj.VersionID, err))
    }
}
return errors.Join(errs...)
Defensive patterns

Strategy: try-catch

Try / catch

if err := store.DeleteAllVersions(ctx, key); err != nil {
    var mr minio.ErrorResponse
    if errors.As(err, &mr) {
        switch mr.Code {
        case "AccessDenied":
            return fmt.Errorf("need s3:DeleteObjectVersion: %w", err)
        case "InvalidObjectState", "ObjectNotUnderActiveHold":
            return fmt.Errorf("object lock blocks version deletion: %w", err)
        }
    }
    return err
}

Prevention

When it happens

Trigger: DeleteAllVersions/DeletePrefixAllVersions iterating versions where RemoveObject with VersionID fails: permission denied (s3:DeleteObjectVersion), object-lock/retention hold on that version, or transient backend error mid-loop.

Common situations: Buckets with Object Lock / legal hold / retention period blocking version deletion; IAM missing DeleteObjectVersion; S3-compatible backends rejecting versioned deletes; a legally held version in an otherwise deletable prefix.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/2f3909265376000d. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/objectstore/objectstore.go:334

func (m *minioStore) DeletePrefixAllVersions(ctx context.Context, prefix string) error {
	return m.deleteVersions(ctx, prefix, false)
}

func (m *minioStore) deleteVersions(ctx context.Context, keyOrPrefix string, exact bool) error {
	found := false
	for obj := range m.client.ListObjects(ctx, m.bucket, minio.ListObjectsOptions{
		Prefix: keyOrPrefix, Recursive: true, WithVersions: true,
	}) {
		if obj.Err != nil {
			return fmt.Errorf("objectstore: list versions %q: %w", keyOrPrefix, obj.Err)
		}
		if exact && obj.Key != keyOrPrefix {
			continue
		}
		found = true
		if err := m.client.RemoveObject(ctx, m.bucket, obj.Key, minio.RemoveObjectOptions{VersionID: obj.VersionID}); err != nil {
			return fmt.Errorf("objectstore: delete version %q (%q): %w", obj.Key, obj.VersionID, err)
		}
	}
	if exact && !found {
		// Non-versioned/S3-compatible backends may omit version listings. Ordinary
		// deletion remains idempotent and covers that deployment shape.
		return m.Delete(ctx, keyOrPrefix)
	}
	return nil
}

func (m *minioStore) Exists(ctx context.Context, key string) (bool, error) {
	_, err := m.client.StatObject(ctx, m.bucket, key, minio.StatObjectOptions{})
	if err != nil {
		if minio.ToErrorResponse(err).Code == "NoSuchKey" {
			return false, nil
		}
		return false, fmt.Errorf("objectstore: stat %q: %w", key, err)
	}

View on GitHub (pinned to 3ee70a1026)