Skip to content

fix(aws): fail adoption gracefully when Cloud Control resource cannot be read back - #1447

Merged
sam-goodwin merged 2 commits into
mainfrom
claude/cc-adopt-empty-readback
Jul 8, 2026
Merged

fix(aws): fail adoption gracefully when Cloud Control resource cannot be read back#1447
sam-goodwin merged 2 commits into
mainfrom
claude/cc-adopt-empty-readback

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

In the Cloud Control adopt path, when CreateResource fails with AlreadyExists but GetResource on the conflicting identifier returns nothing, a non-null assertion let undefined flow into the patch computation:

if (error instanceof AlreadyExistsError && props.adopt) {
  const resource = (await client.getResource(
    props.typeName,
    error.progressEvent.Identifier!,
  ))!; // ← undefined when the resource isn't visible in this account

  response = await updateResourceWithPatch(client, ..., resource, ...);
  // → filterReadOnlyProperties → Object.entries(undefined)
  // → TypeError: Cannot convert undefined or null to object
}

The same unchecked pattern existed in the ConcurrentOperationError adoption branch.

Root cause investigation

This is trivially reachable with S3: bucket names live in a globally shared namespace, so CreateResource reports AlreadyExists when any AWS account owns the name — but GetResource (scoped to the caller's account) finds nothing to adopt:

progressEvent: {
  ErrorCode: "AlreadyExists",
  StatusMessage: "The requested bucket name is not available. The bucket
    namespace is shared by all users of the system. ...",
}
getResource read-back: undefined

The existing test replace resource when immutable property changes was failing deterministically with this TypeError because its deterministic bucket name (<prefix>-test-bucket-resource-immutable-test) is squatted outside the test account.

Fix

Both adoption branches now go through a getResourceForAdoption helper that:

  1. retries the read-back briefly with backoff (absorbs genuine eventual consistency between the create conflict and GetResource), and
  2. throws an actionable error — with the original conflict as cause — when the resource cannot be read back, since adoption is impossible:
Error: Resource AWS::S3::Bucket "my-bucket" already exists but could not be
read back for adoption. It may belong to a different AWS account (e.g. S3
bucket names are globally unique), exist in a different region, or its name
may still be reserved by a recent deletion. Choose a different name or wait
for the name to become available.
Caused by: Error: The requested bucket name is not available. ... (Status Code: 409)

Usage

No API change — the failure mode just becomes diagnosable:

// Bucket name owned by another AWS account
await CloudControlResource("my-bucket", {
  typeName: "AWS::S3::Bucket",
  desiredState: { BucketName: "some-name-taken-by-someone-else" },
  adopt: true,
});
// Before: TypeError: Cannot convert undefined or null to object
// After:  Error: Resource AWS::S3::Bucket "some-name-taken-by-someone-else"
//         already exists but could not be read back for adoption. ...

Testing

  • Verified the new error path against live AWS with a squatted bucket name — the run fails with the descriptive adoption error (409 attached as cause) instead of the TypeError.
  • On unmodified main, 6 of the 10 tests in test/aws/control/ fail in my environment: every S3 bucket name these suites have historically used (samgoodwin-test-bucket-resource*, samgoodwin-test-bucket-proxy*) is taken in the global namespace by an unknown account, so creation fails with AlreadyExists while the bucket is invisible to the test account — which is exactly the crash this PR fixes. This PR renames the base testId in both test files so all derived bucket names are fresh.
  • With the fix + renames, the full test/aws/control/ suite passes against live AWS:
    ✓ alchemy/test/aws/control/proxy.test.ts (6 tests) 50.8s
    ✓ alchemy/test/aws/control/resource.test.ts (4 tests) 52.2s
    Tests  10 passed (10)
    
  • Also verified the local merge of this branch with fix(aws): replace Cloud Control resource on empty read-back during update #1446: the combined suite (11 tests, including fix(aws): replace Cloud Control resource on empty read-back during update #1446's new replacement test) passes against live AWS with no conflicts.

🤖 Generated with Claude Code

… be read back

When CreateResource fails with AlreadyExists but GetResource returns
nothing for the conflicting identifier, the adopt path crashed with
"TypeError: Cannot convert undefined or null to object" — a non-null
assertion let undefined flow into the patch computation. This happens
when the name is taken in a globally shared namespace but not visible
in the caller's account (e.g. an S3 bucket owned by another AWS
account), which Cloud Control also reports as AlreadyExists.

Retry the read-back briefly to absorb eventual consistency, then throw
an actionable error (with the original conflict as cause) when the
resource genuinely cannot be adopted. Applies to both the
AlreadyExistsError and ConcurrentOperationError adoption branches.

Also rename the bucket used by the "replace resource when immutable
property changes" test: its previous deterministic name is squatted in
the global S3 namespace, making the test fail unconditionally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Jul 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/alchemy@1447

commit: 5580277

Every S3 bucket name previously used by these suites is taken in the
global namespace by an unknown account, so creation fails with
AlreadyExists while the bucket is invisible to the test account. Rename
the base testIds so all derived bucket names are fresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sam-goodwin
sam-goodwin merged commit b2505e9 into main Jul 8, 2026
4 of 6 checks passed
@sam-goodwin
sam-goodwin deleted the claude/cc-adopt-empty-readback branch July 8, 2026 06:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant