hashicorp/terraform · error

failed to empty bucket

Error message

failed to empty bucket %v: %v

What it means

Returned by deleteBucket(recursive=true) when listing the bucket's objects via getBucket("") fails with an error whose message does NOT contain the substring 'not exists'. The backend needs to empty the bucket before deleting it; if listing fails for a non-absence reason, it cannot proceed.

Solutions

  1. Inspect the wrapped error for the underlying list failure cause.
  2. Grant `cos:GetBucket` (list) permission to the principal so the bucket can be enumerated for emptying.
  3. For transient failures, retry the destroy.
  4. If you intend to delete the bucket manually, do so from the console after emptying it, then remove the backend resource from config.

Example fix

// before: policy omits cos:GetBucket, recursive delete fails to empty
// after: add list permission
{
  "statement":[{"effect":"allow","action":["cos:GetBucket","cos:DeleteObject","cos:DeleteBucket"],"resource":["qcs::cos:<region>::<bucket>-<appid>","qcs::cos:<region>::<bucket>-<appid>/*"]}]
}
Defensive patterns

Strategy: validation

Validate before calling

// Before recursive delete, confirm list permission on the bucket
func canListBucket(ctx context.Context, client *cos.Client, bucket string) error {
    _, rsp, err := client.Bucket.Get(ctx, &cos.BucketGetOptions{Prefix: ""})
    if rsp == nil { return err }
    defer rsp.Body.Close()
    if rsp.StatusCode == 403 { return fmt.Errorf("principal lacks cos:GetBucket; cannot empty bucket %s", bucket) }
    return nil
}

Type guard

func isListForbidden(status int) bool { return status == 403 }

Try / catch

// If emptying fails with a permission error, surface a clear message
if rsp != nil && rsp.StatusCode == 403 {
    return backoff.Permanent(fmt.Errorf("grant cos:GetBucket to empty bucket %s: %w", bucket, err))
}

Prevention

When it happens

Trigger: Inside deleteBucket, getBucket("") returns an error that is not the bucket-absence error. Typical: 403 on `cos:GetBucket`, a transient 5xx, or a network failure mapped to a non-'not exists' message.

Common situations: Running `terraform destroy` against a COS state backend where the principal can delete the bucket but lacks list permission; partial outage of the COS list API; credentials were downgraded between apply and destroy.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/bdc805d40f5bda14. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/cos/client.go:333

	}

	if err != nil {
		return fmt.Errorf("failed to create bucket %v: %v", c.bucket, err)
	}

	return nil
}

// deleteBucket delete cos bucket
func (c *remoteClient) deleteBucket(recursive bool) error {
	if recursive {
		obs, err := c.getBucket("")
		if err != nil {
			if strings.Contains(err.Error(), "not exists") {
				return nil
			}
			log.Printf("[DEBUG] deleteBucket %s: empty bucket error: %v", c.bucket, err)
			return fmt.Errorf("failed to empty bucket %v: %v", c.bucket, err)
		}
		for _, v := range obs {
			c.deleteObject(v.Key)
		}
	}

	rsp, err := c.cosClient.Bucket.Delete(c.cosContext)
	if rsp == nil {
		log.Printf("[DEBUG] deleteBucket %s: error: %v", c.bucket, err)
		return fmt.Errorf("failed to delete bucket %v: %v", c.bucket, err)
	}
	defer rsp.Body.Close()

	log.Printf("[DEBUG] deleteBucket %s: code: %d, error: %v", c.bucket, rsp.StatusCode, err)
	if rsp.StatusCode == 404 {
		return nil
	}

View on GitHub (pinned to d32a084675)