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
- Inspect the wrapped error for dial/TLS/context causes
- Confirm the bucket endpoint is reachable from the runner (curl the bucket URL)
- Fix network/proxy and retry the operation — the prior state object remains intact
- 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
- Check PUT reachability through the proxy/egress firewall before CI runs
- A failed Put leaves the previous state intact — safe to retry the whole command
- Keep states lean to reduce upload windows on unstable links
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
- failed to open file at %v: %w
- failed to open file at %v: checksum mismatch, %s != %s
- failed to delete file %v: %w
- bucket %s not exists
- failed to create bucket %v: %w
AI-assisted analysis of opentofu/opentofu@3561785c48 (2026-08-15).
Data as JSON: /api/errors/f10d4d46c0357cb4.
Report an issue: GitHub.