Skip to content
7 changes: 7 additions & 0 deletions .changeset/shiny-ghosts-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
---

fix: stop getPlatformProxy crashing when internal DOs are present

Internal DOs still do not work with getPlatformProxy, but warn instead of crashing.
56 changes: 55 additions & 1 deletion fixtures/get-platform-proxy/tests/get-platform-proxy.env.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import path from "path";
import { D1Database, R2Bucket } from "@cloudflare/workers-types";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
afterEach,
beforeEach,
describe,
expect,
it,
MockInstance,
vi,
} from "vitest";
import { getPlatformProxy } from "./shared";
import type { Hyperdrive, KVNamespace } from "@cloudflare/workers-types";
import type { Unstable_DevWorker } from "wrangler";
Expand Down Expand Up @@ -197,6 +205,52 @@ describe("getPlatformProxy - env", () => {
}
});

describe("DO warnings", () => {
let warn = {} as MockInstance<typeof console.warn>;
beforeEach(() => {
warn = vi.spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
warn.mockRestore();
});

it("warns about internal DOs and doesn't crash", async () => {
await getPlatformProxy<Env>({
configPath: path.join(__dirname, "..", "wrangler_internal_do.jsonc"),
});
expect(warn).toMatchInlineSnapshot(`
[MockFunction warn] {
"calls": [
[
"▲ [WARNING]  You have defined bindings to the following internal Durable Objects:
- {"class_name":"MyDurableObject","name":"MY_DURABLE_OBJECT"}
These will not work in local development, but they should work in production.
If you want to develop these locally, you can define your DO in a separate Worker, with a separate configuration file.
For detailed instructions, refer to the Durable Objects section here: https://developers.cloudflare.com/workers/wrangler/api#supported-bindings
",
],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
}
`);
});

it("doesn't warn about external DOs and doesn't crash", async () => {
await getPlatformProxy<Env>({
configPath: path.join(__dirname, "..", "wrangler_external_do.jsonc"),
});
expect(warn).not.toHaveBeenCalled();
});
});

describe("with a target environment", () => {
it("should provide bindings targeting a specified environment and also inherit top-level ones", async () => {
const { env, dispose } = await getPlatformProxy<Env>({
Expand Down
24 changes: 24 additions & 0 deletions fixtures/get-platform-proxy/wrangler_external_do.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "external-do",
"main": "src/index.ts",
// external durable object = binding has script_name. This indicates that
// the DO is exported in a separate Worker called `do-worker`. For the
// purposes of testing, we don't need to set that up because
// getPlatformProxy would not be involved in running that Worker.

"durable_objects": {
"bindings": [
{
"class_name": "MyDurableObject",
"name": "MY_DURABLE_OBJECT",
"script_name": "do-worker",
},
],
},
"migrations": [
{
"new_sqlite_classes": ["MyDurableObject"],
"tag": "v1",
},
],
}
23 changes: 23 additions & 0 deletions fixtures/get-platform-proxy/wrangler_internal_do.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "internal-do",
"main": "src/index.ts",
// Internal durable object = the binding does not specify a script name.
// This implies the DO is exported alongside this worker in `index.ts`,
// which it isn't actually. However we don't care about this here because
// getPlatformProxy will discard all user code anyway. We are simply making
// sure the warning shows up.
"durable_objects": {
"bindings": [
{
"class_name": "MyDurableObject",
"name": "MY_DURABLE_OBJECT",
},
],
},
"migrations": [
{
"new_sqlite_classes": ["MyDurableObject"],
"tag": "v1",
},
],
}
5 changes: 0 additions & 5 deletions packages/wrangler/e2e/__snapshots__/pages-dev.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

exports[`Pages 'wrangler pages dev' > should merge (with override) \`wrangler.toml\` configuration with configuration provided via the command line, with command line args taking precedence 1`] = `
"✨ Compiled Worker successfully
▲ [WARNING] WARNING: You have Durable Object bindings that are not defined locally in the worker being developed.
Be aware that changes to the data stored in these Durable Objects will be permanent and affect the live instances.
Remote Durable Objects that are affected:
- {"name":"DO_BINDING_1_TOML","class_name":"DO_1_TOML","script_name":"DO_SCRIPT_1_TOML"}
- {"name":"DO_BINDING_2_TOML","class_name":"DO_2_TOML","script_name":"DO_SCRIPT_2_TOML"}
Your Worker and resources are simulated locally via Miniflare. For more information, see: https://developers.cloudflare.com/workers/testing/local-development.
Your worker has access to the following bindings:
- Durable Objects:
Expand Down
9 changes: 0 additions & 9 deletions packages/wrangler/src/__tests__/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1288,15 +1288,6 @@ describe.sequential("wrangler dev", () => {
https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/ for more
details.


▲ [WARNING] WARNING: You have Durable Object bindings that are not defined locally in the worker being developed.

Be aware that changes to the data stored in these Durable Objects will be permanent and affect the
live instances.
Remote Durable Objects that are affected:
- {\\"name\\":\\"NAME_2\\",\\"class_name\\":\\"CLASS_2\\",\\"script_name\\":\\"SCRIPT_A\\"}
- {\\"name\\":\\"NAME_4\\",\\"class_name\\":\\"CLASS_4\\",\\"script_name\\":\\"SCRIPT_B\\"}

"
`);
});
Expand Down
20 changes: 19 additions & 1 deletion packages/wrangler/src/api/integrations/platform/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { kCurrentWorker, Miniflare } from "miniflare";
import { getAssetsOptions } from "../../../assets";
import { readConfig } from "../../../config";
import { partitionDurableObjectBindings } from "../../../deployment-bundle/entry";
import { DEFAULT_MODULE_RULES } from "../../../deployment-bundle/rules";
import { getBindings } from "../../../dev";
import { getBoundRegisteredWorkers } from "../../../dev-registry";
Expand All @@ -11,7 +12,9 @@ import {
buildSitesOptions,
} from "../../../dev/miniflare";
import { run } from "../../../experimental-flags";
import { logger } from "../../../logger";
import { getSiteAssetPaths } from "../../../sites";
import { dedent } from "../../../utils/dedent";
import { CacheStorage } from "./caches";
import { ExecutionContext } from "./executionContext";
import { getServiceBindings } from "./services";
Expand Down Expand Up @@ -129,21 +132,35 @@ export async function getPlatformProxy<
};
}

// this is only used by getPlatformProxy
async function getMiniflareOptionsFromConfig(
rawConfig: Config,
env: string | undefined,
options: GetPlatformProxyOptions
): Promise<Partial<MiniflareOptions>> {
const bindings = getBindings(rawConfig, env, true, {});

if (rawConfig["durable_objects"]) {
const { localBindings } = partitionDurableObjectBindings(rawConfig);
if (localBindings.length > 0) {
logger.warn(dedent`
You have defined bindings to the following internal Durable Objects:
${localBindings.map((b) => `- ${JSON.stringify(b)}`).join("\n")}
These will not work in local development, but they should work in production.

If you want to develop these locally, you can define your DO in a separate Worker, with a separate configuration file.
For detailed instructions, refer to the Durable Objects section here: https://developers.cloudflare.com/workers/wrangler/api#supported-bindings
`);
}
}
const workerDefinitions = await getBoundRegisteredWorkers({
name: rawConfig.name,
services: bindings.services,
durableObjects: rawConfig["durable_objects"],
});

const { bindingOptions, externalWorkers } = buildMiniflareBindingOptions({
name: undefined,
name: rawConfig.name,
bindings,
workerDefinitions,
queueConsumers: undefined,
Expand All @@ -162,6 +179,7 @@ async function getMiniflareOptionsFromConfig(
{
script: "",
modules: true,
name: rawConfig.name,
...bindingOptions,
serviceBindings: {
...serviceBindings,
Expand Down
20 changes: 6 additions & 14 deletions packages/wrangler/src/deployment-bundle/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import path from "node:path";
import dedent from "ts-dedent";
import { configFileName, formatConfigSnippet } from "../config";
import { UserError } from "../errors";
import { logger } from "../logger";
import { sniffUserAgent } from "../package-manager";
import guessWorkerFormat from "./guess-worker-format";
import {
Expand Down Expand Up @@ -130,17 +129,7 @@ export async function getEntry(
config.tsconfig
);

const { localBindings, remoteBindings } =
partitionDurableObjectBindings(config);

if (command === "dev" && remoteBindings.length > 0) {
logger.warn(
"WARNING: You have Durable Object bindings that are not defined locally in the worker being developed.\n" +
"Be aware that changes to the data stored in these Durable Objects will be permanent and affect the live instances.\n" +
"Remote Durable Objects that are affected:\n" +
remoteBindings.map((b) => `- ${JSON.stringify(b)}`).join("\n")
);
}
const { localBindings } = partitionDurableObjectBindings(config);

if (format === "service-worker" && localBindings.length > 0) {
const errorMessage =
Expand Down Expand Up @@ -171,14 +160,17 @@ export async function getEntry(
* Groups the durable object bindings into two lists:
* those that are defined locally and those that refer to a durable object defined in another script.
*/
function partitionDurableObjectBindings(config: Config): {
export function partitionDurableObjectBindings(config: Config): {
localBindings: DurableObjectBindings;
remoteBindings: DurableObjectBindings;
} {
const localBindings: DurableObjectBindings = [];
const remoteBindings: DurableObjectBindings = [];
for (const binding of config.durable_objects.bindings) {
if (binding.script_name === undefined) {
if (
binding.script_name === undefined ||
binding.script_name === config.name
) {
localBindings.push(binding);
} else {
remoteBindings.push(binding);
Expand Down
Loading