Skip to content

Commit feb0de0

Browse files
committed
Recognize unuploaded dispatch scripts when provisioning Durable Objects
1 parent 24a1d4c commit feb0de0

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

‎platform/packages/api/src/provisioning/durable-objects.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,17 @@ function marker(
6060

6161
async function readBindings(env: Env, worker: string): Promise<Array<Binding> | undefined> {
6262
try {
63-
return await cfFetch<Array<Binding>>(`${url(env, worker)}/bindings`, env.CF_API_TOKEN);
63+
const bindings = await cfFetch<Array<Binding>>(
64+
`${url(env, worker)}/bindings`,
65+
env.CF_API_TOKEN,
66+
);
67+
// Dispatch can return an empty binding list for a script without a Worker.
68+
// Confirm absence before provisioning; an existing unmarked Worker must
69+
// still fail the ownership check.
70+
if (bindings.length === 0) {
71+
await cfFetch(`${url(env, worker)}/settings`, env.CF_API_TOKEN);
72+
}
73+
return bindings;
6474
} catch (error) {
6575
if (error instanceof CloudflareApiError && error.status === 404) {
6676
return undefined;

‎platform/packages/api/test/integration/managed-durable-objects.test.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,20 @@ beforeEach(async () => {
7878
const name = /\/scripts\/([^/?]+)/.exec(url)?.[1];
7979
if (
8080
name?.startsWith('void-do-') &&
81-
url.endsWith('/bindings') &&
81+
(url.endsWith('/bindings') || url.endsWith('/settings')) &&
8282
!provider.state.hostScripts.has(name)
8383
) {
84+
if (url.endsWith('/bindings')) {
85+
return cfSuccess([]);
86+
}
8487
return Response.json(
8588
{ success: false, errors: [{ code: 10007, message: 'script not found' }] },
8689
{ status: 404 },
8790
);
8891
}
92+
if (name?.startsWith('void-do-') && url.endsWith('/settings') && !init?.method) {
93+
return cfSuccess({ bindings: provider.state.hostScripts.get(name)!.bindings });
94+
}
8995
const response = await original(input, init);
9096
if (init?.method === 'PUT' && init.body instanceof FormData && response.ok && name) {
9197
const metadata = JSON.parse(await (init.body.get('metadata') as Blob).text()) as Record<
@@ -190,6 +196,21 @@ function currentRelease(worker: string) {
190196
}
191197

192198
describe('managed native Durable Objects', () => {
199+
it('refuses to replace an existing Worker whose bindings are empty', async () => {
200+
const worker = await durableObjectWorkerName(project.id, binding.className);
201+
provider.state.hostScripts.set(worker, {
202+
classes: new Map(),
203+
bindings: [],
204+
metadata: {},
205+
});
206+
const result = await deploy();
207+
expect(result.events).toContainEqual(
208+
expect.objectContaining({ event: 'error', message: expect.stringContaining('ownership') }),
209+
);
210+
expect(writes).toHaveLength(0);
211+
expect(provider.state.hostScripts.get(worker)!.bindings).toEqual([]);
212+
});
213+
193214
it('runs the published class through real RPC and retains data after upgrade, rollback, and restart', async () => {
194215
const directory = mkdtempSync(join(tmpdir(), 'void-managed-native-'));
195216
let runtime: Miniflare | undefined;

0 commit comments

Comments
 (0)