{"record":{"id":"dfedd2d54d0871db","repo":"hashicorp/terraform","slug":"failed-to-retrieve-lock-info-for-lock-id-q-s","errorCode":null,"errorMessage":"failed to retrieve lock info for lock ID %q: %s","messagePattern":"failed to retrieve lock info for lock ID %q: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/s3/client.go","lineNumber":567,"sourceCode":"\t\tKey:    aws.String(c.lockFilePath),\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete the lock file: %w\", err)\n\t}\n\n\tlog.Debug(fmt.Sprintf(\"Deleted lock file: '%q'\", c.lockFilePath))\n\n\treturn nil\n}\n\nfunc (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error {\n\t// TODO: store the path and lock ID in separate fields, and have proper\n\t// projection expression only delete the lock if both match, rather than\n\t// checking the ID from the info field first.\n\tlockInfo, err := c.getLockInfoWithDynamoDB(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to retrieve lock info for lock ID %q: %s\", id, err)\n\t}\n\tlockErr.Info = lockInfo\n\n\tif lockInfo.ID != id {\n\t\treturn fmt.Errorf(\"lock ID %q does not match existing lock (%q)\", id, lockInfo.ID)\n\t}\n\n\tparams := &dynamodb.DeleteItemInput{\n\t\tKey: map[string]dynamodbtypes.AttributeValue{\n\t\t\t\"LockID\": &dynamodbtypes.AttributeValueMemberS{\n\t\t\t\tValue: c.lockPath(),\n\t\t\t},\n\t\t},\n\t\tTableName: aws.String(c.ddbTable),\n\t}\n\t_, err = c.dynClient.DeleteItem(ctx, params)\n\n\tif err != nil {","sourceCodeStart":549,"sourceCodeEnd":585,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/s3/client.go#L549-L585","documentation":"Thrown inside unlockWithDynamoDB when the prerequisite getLockInfoWithDynamoDB call fails. Terraform reads the existing lock info from DynamoDB before deleting the row, both to verify ID ownership and to populate lockErr.Info for diagnostics. If the read fails, the delete never happens.","triggerScenarios":"c.getLockInfoWithDynamoDB at client.go:565 returns an error. Triggers: DynamoDB GetItem error (table deleted, AccessDenied on dynamodb:GetItem, throttling, region mismatch), or the JSON unmarshal of the Info attribute fails inside getLockInfoWithDynamoDB.","commonSituations":"DynamoDB lock table deleted or renamed after the lock was taken, IAM principal lost dynamodb:GetItem, provisioned-capacity throttling on read, the Info attribute corrupted, or cross-account access where the role lacks read on the table.","solutions":["Confirm the table exists in-region: `aws dynamodb describe-table --table-name <table>`.","Verify dynamodb:GetItem on the table for the principal.","Inspect the wrapped error in the %s segment to distinguish AccessDenied from ResourceNotFoundException vs. unmarshal failure.","If the Info attribute is corrupted, delete the row directly: `aws dynamodb delete-item --table-name <table> --key '{\"LockID\":{\"S\":\"<bucket>/<path>\"}}'`.","Retry force-unlock after restoring table access; transient throttling clears on backoff."],"exampleFix":"# bypass the failed read-info step by deleting the DDB lock row directly\naws dynamodb delete-item \\\n  --table-name terraform-locks \\\n  --key '{\"LockID\":{\"S\":\"tf-state-prod/prod/terraform.tfstate\"}}'","handlingStrategy":"validation","validationCode":"// Before unlock, confirm the DDB lock row is readable.\nfunc ddbLockReadable(ctx context.Context, c *dynamodb.Client, table, lockPath string) error {\n  _, err := c.GetItem(ctx, &dynamodb.GetItemInput{\n    Key: map[string]types.AttributeValue{\"LockID\": &types.AttributeValueMemberS{Value: lockPath}},\n    TableName: &table, ProjectionExpression: aws.String(\"LockID, Info\"),\n  })\n  return err\n}","typeGuard":null,"tryCatchPattern":"// If getLockInfoWithDynamoDB fails with ResourceNotFound, the row is already gone (benign).\nlockInfo, err := c.getLockInfoWithDynamoDB(ctx)\nif err != nil {\n  var apiErr smithy.APIError\n  if errors.As(err, &apiErr) && apiErr.ErrorCode() == \"ResourceNotFoundException\" {\n    return nil // row already absent\n  }\n  return fmt.Errorf(\"failed to retrieve lock info for lock ID %q: %s; \"+\n    \"delete row LockID=%s manually if needed\", id, err, c.lockPath())\n}","preventionTips":["Grant dynamodb:GetItem on the lock table in the apply role.","Keep the lock table's name stable and version-controlled.","Use `terraform force-unlock <id>` after crashes to clear DDB rows promptly.","Monitor DDB read capacity; use on-demand billing under contention."],"tags":["locking","dynamodb","remote-state","getitem","iam","unlock"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}