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
- Confirm the state server returns the standard Terraform HTTP lock-info JSON ({ID, Operation, Who, Created, ...}) on 409/423.
- Reproduce with `curl -i -X LOCK <lock_address>` while another run holds the lock and inspect the Content-Type/body.
- Bypass any error-page-rendering proxy for the state path.
- 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
- Ensure the state server returns the Terraform lock-info JSON shape on 409/423.
- Bypass error-page-rendering proxies for the state path.
- Pin state-server version to one with a known lock-info schema.
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
- HTTP remote state endpoint requires auth
- HTTP remote state already locked, failed to read body
- Unexpected HTTP response code %d
- Failed to make %s HTTP request: %s
- HTTP remote state endpoint invalid auth
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/0e1f2cf8385fb8c9.
Report an issue: GitHub.