hashicorp/terraform · error · statemgr.LockError

HTTP remote state already locked, failed to unmarshal body

Error message

HTTP remote state already locked, failed to unmarshal body

What it means

On a 409/423 response the body was read successfully, but json.Unmarshal into statemgr.LockInfo failed. The server claimed the state was locked but returned a body that is not the JSON lock-info document the backend expects, so the holder's identity cannot be recovered. Returned as a statemgr.LockError.

Source

Thrown at internal/backend/remote-state/http/client.go:114

		c.jsonLockInfo = jsonLockInfo
		return info.ID, nil
	case http.StatusUnauthorized:
		return "", fmt.Errorf("HTTP remote state endpoint requires auth")
	case http.StatusForbidden:
		return "", fmt.Errorf("HTTP remote state endpoint invalid auth")
	case http.StatusConflict, http.StatusLocked:
		defer resp.Body.Close()
		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return "", &statemgr.LockError{
				Err: fmt.Errorf("HTTP remote state already locked, failed to read body"),
			}
		}
		existing := statemgr.LockInfo{}
		err = json.Unmarshal(body, &existing)
		if err != nil {
			return "", &statemgr.LockError{
				Err: fmt.Errorf("HTTP remote state already locked, failed to unmarshal body"),
			}
		}
		return "", &statemgr.LockError{
			Info: &existing,
			Err:  fmt.Errorf("HTTP remote state already locked: ID=%s", existing.ID),
		}
	default:
		return "", fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
	}
}

func (c *httpClient) Unlock(id string) error {
	if c.UnlockURL == nil {
		return nil
	}

	resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock")
	if err != nil {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Confirm the state server returns the standard Terraform HTTP lock-info JSON ({ID, Operation, Who, Created, ...}) on 409/423.
  2. Reproduce with `curl -i -X LOCK <lock_address>` while another run holds the lock and inspect the Content-Type/body.
  3. Bypass any error-page-rendering proxy for the state path.
  4. If the lock is stale and the holder is unknown server-side, identify the lock record on the server and `terraform force-unlock <ID>`.
Defensive patterns

Strategy: try-catch

Type guard

func isLockUnmarshalErr(err error) bool {
  var le *statemgr.LockError
  return errors.As(err, &le) && strings.Contains(le.Err.Error(), "failed to unmarshal body")
}

Try / catch

err = sm.Lock(info)
if isLockUnmarshalErr(err) {
  // server did not return lock-info JSON; surface actionable message
  log.Printf("lock response not JSON lock-info; inspect server config: %v", err)
  return err
}

Prevention

When it happens

Trigger: Custom state server returns plain text or HTML on 409 (e.g. a reverse-proxy error page) instead of the Terraform lock-info JSON; version skew where the server emits a newer/older lock-info schema; body contains a BOM or leading garbage.

Common situations: Reverse proxy (nginx/ALB) serving a static 409 page; server upgraded its lock-info format; misconfigured WAF rewriting error bodies.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/0e1f2cf8385fb8c9. Report an issue: GitHub.