hashicorp/terraform · error · LockError

HTTP remote state already locked, failed to unmarshal body

Error message

HTTP remote state already locked, failed to unmarshal body

What it means

The lock endpoint returned 409 Conflict or 423 Locked and the body was read successfully, but json.Unmarshal into statemgr.LockInfo failed. The server returned a 409/423 body that is not the JSON lock-info document Terraform expects (fields: ID, Operation, Who, Created, Path, Info). Wrapped in a statemgr.LockError.

Solutions

  1. Make the server return Terraform's LockInfo JSON shape (at minimum an 'ID' field) on 409/423 responses.
  2. If you cannot change the server, disable HTTP locking by omitting lock_address/unlock_address.
  3. Inspect the actual 409/423 body with curl -i -X <lock_method> <lock_address> to see what the server sends.
  4. Switch to an HTTP backend implementation that conforms to the Terraform HTTP backend contract.
Defensive patterns

Strategy: validation

Validate before calling

# Inspect the actual body the server returns on a conflict, then decide whether to
# fix the server or disable locking.
curl -sS -u "$TF_HTTP_USERNAME:$TF_HTTP_PASSWORD" -X "${TF_HTTP_LOCK_METHOD:-LOCK}" \
  -i "${TF_HTTP_LOCK_ADDRESS:-$TF_HTTP_ADDRESS}" | sed -n '1,40p'

Prevention

When it happens

Trigger: Server returns an HTML error page, plain text, or a JSON envelope that does not match the LockInfo schema on 409/423; a reverse proxy serves a canned error page for conflict responses; the HTTP backend implementation does not follow Terraform's lock-info contract.

Common situations: Custom/3rd-party HTTP backend that signals 409 but returns its own error format; CDN/WAF replaces the body with a static page; server returns JSON like {"error":"locked"} without the ID field.

Related errors


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

Appendix: 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 d32a084675)