hashicorp/terraform · error

failed to read the body of the S3 object

Error message

failed to read the body of the S3 object: %w

What it means

Thrown inside unlockWithFile after GetObject succeeded but reading the response body stream failed. The S3 GET connection was established and headers received, yet the body transfer was interrupted before io.ReadAll completed.

Solutions

  1. Retry the unlock operation — body-read failures are almost always transient.
  2. If recurring, check network egress (proxy, NAT, VPN) for connection truncation and raise client-side timeouts.
  3. Run with TF_LOG=DEBUG to see whether the connection is reset by peer or timed out, then tune accordingly.
  4. Ensure the process is not being cancelled mid-operation (avoid Ctrl-C during unlock); if interrupted, re-run force-unlock.
  5. If the lock file is small and the failure persists, manually fetch then delete it via the AWS CLI to unblock.

Example fix

# retry is the fix; if it keeps failing, fetch+delete directly
aws s3api get-object --bucket tf-state-prod --key prod/terraform.tflock.tflock /tmp/lock.json
aws s3api delete-object --bucket tf-state-prod --key prod/terraform.tflock.tflock
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight network check is limited; instead set a per-call read deadline.
import "time"
func readWithTimeout(r io.Reader, d time.Duration) ([]byte, error) {
  type res struct { b []byte; e error }
  ch := make(chan res, 1)
  go func() { b, e := io.ReadAll(r); ch <- res{b, e} }()
  select {
  case <-time.After(d): return nil, errors.New("read timeout")
  case v := <-ch: return v.b, v.e
  }
}

Try / catch

// Retry body reads a couple of times for transient transport errors.
var data []byte
var err error
for i := 0; i < 3; i++ {
  // re-Get each retry because the body stream is consumed
  getOutput, gerr := c.s3Client.GetObject(ctx, getInput)
  if gerr != nil { return gerr }
  data, err = io.ReadAll(getOutput.Body)
  getOutput.Body.Close()
  if err == nil { break }
  time.Sleep(backoff(i))
}

Prevention

When it happens

Trigger: io.ReadAll(getOutput.Body) at client.go:530 returns an error. Triggers: network connection reset or timeout mid-stream, the S3 endpoint closed the socket early, a proxy/load-balancer truncating the response, or the context being cancelled while the body was still draining.

Common situations: Flaky network or VPN dropping long-lived connections, an HTTP proxy with aggressive idle timeouts, constrained-bandwidth CI runners, or a parent context cancellation (e.g. user Ctrl-C) interrupting the read.

Related errors


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

Appendix: source

Thrown at internal/backend/remote-state/s3/client.go:532

	if c.serverSideEncryption && c.customerEncryptionKey != nil {
		getInput.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey))
		getInput.SSECustomerAlgorithm = aws.String(s3EncryptionAlgorithm)
		getInput.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5())
	}

	getOutput, err := c.s3Client.GetObject(ctx, getInput)
	if err != nil {
		return fmt.Errorf("unable to retrieve file from S3 bucket '%s' with key '%s': %w", c.bucketName, c.lockFilePath, err)
	}
	defer func() {
		if cerr := getOutput.Body.Close(); cerr != nil {
			log.Warn(fmt.Sprintf("failed to close S3 object body: %v", cerr))
		}
	}()

	data, err := io.ReadAll(getOutput.Body)
	if err != nil {
		return fmt.Errorf("failed to read the body of the S3 object: %w", err)
	}

	lockInfo := &statemgr.LockInfo{}
	if err := json.Unmarshal(data, lockInfo); err != nil {
		return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
	}
	lockErr.Info = lockInfo

	// Verify that the provided lock ID matches the lock ID of the retrieved lock file.
	if lockInfo.ID != id {
		return fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID)
	}

	// Delete the lock file to release the lock.
	_, err = c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(c.bucketName),
		Key:    aws.String(c.lockFilePath),
	})

View on GitHub (pinned to d32a084675)