opentofu/opentofu · error

failed to save file to %v: %w

Error message

failed to save file to %v: %w

What it means

putObject got a nil response from Object.Put: the request never completed at the HTTP level, so the state (or lock file) was not persisted and the raw transport error is wrapped with the target path. Any tofu operation that writes state (apply persistence, state push, lock creation) can hit this. A failed Put does not damage the previous state object.

Source

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

		ObjectPutHeaderOptions: &cos.ObjectPutHeaderOptions{
			XCosMetaXXX: &http.Header{
				"X-Cos-Meta-Md5": []string{fmt.Sprintf("%x", md5.Sum(data))},
			},
		},
		ACLHeaderOptions: &cos.ACLHeaderOptions{
			XCosACL: c.acl,
		},
	}

	if c.encrypt {
		opt.ObjectPutHeaderOptions.XCosServerSideEncryption = "AES256"
	}

	r := bytes.NewReader(data)
	rsp, err := c.cosClient.Object.Put(ctx, cosFile, r, opt)
	if rsp == nil {
		log.Printf("[DEBUG] putObject %s: error: %v", cosFile, err)
		return fmt.Errorf("failed to save file to %v: %w", cosFile, err)
	}
	defer rsp.Body.Close()

	log.Printf("[DEBUG] putObject %s: code: %d, error: %v", cosFile, rsp.StatusCode, err)
	if err != nil {
		return fmt.Errorf("failed to save file to %v: %w", cosFile, err)
	}

	return nil
}

// deleteObject delete remote object
func (c *remoteClient) deleteObject(ctx context.Context, cosFile string) error {
	rsp, err := c.cosClient.Object.Delete(ctx, cosFile)
	if rsp == nil {
		log.Printf("[DEBUG] deleteObject %s: error: %v", cosFile, err)
		return fmt.Errorf("failed to delete file %v: %w", cosFile, err)
	}

View on GitHub (pinned to 3561785c48)

Solutions

  1. Inspect the wrapped error for dial/TLS/context causes
  2. Confirm the bucket endpoint is reachable from the runner (curl the bucket URL)
  3. Fix network/proxy and retry the operation — the prior state object remains intact
  4. For recurring timeouts on large states, raise client/proxy limits or reduce state size
Defensive patterns

Strategy: retry

Type guard

func isTransportError(err error) bool {
	var nerr net.Error
	if errors.As(err, &nerr) {
		return true
	}
	return strings.Contains(err.Error(), "dial tcp") ||
		strings.Contains(err.Error(), "TLS handshake")
}

Try / catch

for i := 0; i < 3; i++ {
	if err := c.Put(ctx, data); err == nil {
		break
	} else if isTransportError(err) {
		time.Sleep(time.Duration(i+1) * 2 * time.Second)
		continue
	} else {
		return err
	}
}

Prevention

When it happens

Trigger: DNS/TLS failure reaching cos.<region>.myqcloud.com; connection reset during upload; context canceled mid-Put; proxy rejecting PUT requests; SDK failing before send due to broken configuration.

Common situations: VPN drops during long applies; CI egress filters that allow GET but block PUT; very large states timing out at intermediary proxies.

Related errors


AI-assisted analysis of opentofu/opentofu@3561785c48 (2026-08-15). Data as JSON: /api/errors/f10d4d46c0357cb4. Report an issue: GitHub.