{"record":{"id":"36c78b2d9547c3cd","repo":"hashicorp/terraform","slug":"http-remote-state-already-locked-failed-to-read-b","errorCode":null,"errorMessage":"HTTP remote state already locked, failed to read body","messagePattern":"HTTP remote state already locked, failed to read body","errorType":"exception","errorClass":"statemgr.LockError","httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/http/client.go","lineNumber":107,"sourceCode":"\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tswitch resp.StatusCode {\n\tcase http.StatusOK:\n\t\tc.lockID = info.ID\n\t\tc.jsonLockInfo = jsonLockInfo\n\t\treturn info.ID, nil\n\tcase http.StatusUnauthorized:\n\t\treturn \"\", fmt.Errorf(\"HTTP remote state endpoint requires auth\")\n\tcase http.StatusForbidden:\n\t\treturn \"\", fmt.Errorf(\"HTTP remote state endpoint invalid auth\")\n\tcase http.StatusConflict, http.StatusLocked:\n\t\tdefer resp.Body.Close()\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn \"\", &statemgr.LockError{\n\t\t\t\tErr: fmt.Errorf(\"HTTP remote state already locked, failed to read body\"),\n\t\t\t}\n\t\t}\n\t\texisting := statemgr.LockInfo{}\n\t\terr = json.Unmarshal(body, &existing)\n\t\tif err != nil {\n\t\t\treturn \"\", &statemgr.LockError{\n\t\t\t\tErr: fmt.Errorf(\"HTTP remote state already locked, failed to unmarshal body\"),\n\t\t\t}\n\t\t}\n\t\treturn \"\", &statemgr.LockError{\n\t\t\tInfo: &existing,\n\t\t\tErr:  fmt.Errorf(\"HTTP remote state already locked: ID=%s\", existing.ID),\n\t\t}\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"Unexpected HTTP response code %d\", resp.StatusCode)\n\t}\n}\n","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/http/client.go#L89-L125","documentation":"The server returned 409 Conflict or 423 Locked (state is held by another run), so the backend tries to read the JSON lock-info body to report who owns the lock — but io.ReadAll(resp.Body) failed, typically because the connection was closed mid-body or a read timeout fired. The result is a statemgr.LockError wrapping this generic message, so the original holder's identity is unknown.","triggerScenarios":"Concurrent terraform apply on shared state where the lock-info response body is truncated or the connection drops; an interceptor (proxy/IDS) closing 4xx responses early; a server returning 423 with an empty body that the reader surfaces as an unexpected EOF.","commonSituations":"Flaky network between CI and state server; aggressive proxy timeout on error responses; custom state server not returning a body on 409/423.","solutions":["Retry the terraform run; transient body-read failures often succeed on the next attempt.","Inspect the state server logs to confirm it is sending a complete JSON body for 409/423 responses.","Increase proxy/load-balancer client-body timeouts for the state endpoints.","If another run legitimately holds the lock, run `terraform force-unlock <ID>` once you confirm it is safe (you will not have the ID here, so inspect server-side lock records)."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"import \"errors\"\n\nfunc isLockBodyReadErr(err error) bool {\n  var le *statemgr.LockError\n  return errors.As(err, &le) && le.Err != nil && strings.Contains(le.Err.Error(), \"failed to read body\")\n}","tryCatchPattern":"for attempt := 0; attempt < 3; attempt++ {\n  err = sm.Lock(info)\n  if err == nil { break }\n  if isLockBodyReadErr(err) { backoff(); continue }\n  return err\n}","preventionTips":["Increase proxy/server client-body timeouts for state endpoints.","Ensure the state server sends a complete body on 409/423.","Retry transient body-read failures before alerting."],"tags":["locking","http","network","http-backend","terraform"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}