From f2a820692bb930d2c6b3ad07c0fa45c6bdb6d250 Mon Sep 17 00:00:00 2001 From: Karim Sawaneh Date: Wed, 14 Jan 2026 19:09:41 +0100 Subject: [PATCH] [wrangler] Defer route conflict lookup until publish failure --- .changeset/lazy-route-conflict-check.md | 7 + .../wrangler/src/__tests__/deploy.test.ts | 292 ++++-------------- packages/wrangler/src/triggers/deploy.ts | 230 ++++++++------ 3 files changed, 188 insertions(+), 341 deletions(-) create mode 100644 .changeset/lazy-route-conflict-check.md diff --git a/.changeset/lazy-route-conflict-check.md b/.changeset/lazy-route-conflict-check.md new file mode 100644 index 000000000000..ca58a931a9f8 --- /dev/null +++ b/.changeset/lazy-route-conflict-check.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Avoid unnecessary route conflict lookups during deploy + +When deploying with routes, Wrangler now only queries the Zones API to diagnose route conflicts after the bulk routes update fails, instead of doing so on successful deploys. This can reduce deploy latency and API traffic while still surfacing a clear error when a route is already assigned to a different worker. diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index ed9b8784d440..dcf730122517 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -45,10 +45,7 @@ import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; import { mockAuthDomain } from "./helpers/mock-auth-domain"; import { mockConsoleMethods } from "./helpers/mock-console"; import { clearDialogs, mockConfirm, mockPrompt } from "./helpers/mock-dialogs"; -import { - mockGetZones, - mockGetZonesMulti, -} from "./helpers/mock-get-zone-from-host"; +import { mockGetZones } from "./helpers/mock-get-zone-from-host"; import { useMockIsTTY } from "./helpers/mock-istty"; import { mockKeyListRequest, @@ -67,10 +64,7 @@ import { mockSubDomainRequest, mockUpdateWorkerSubdomain, } from "./helpers/mock-workers-subdomain"; -import { - mockGetZoneWorkerRoutes, - mockGetZoneWorkerRoutesMulti, -} from "./helpers/mock-zone-routes"; +import { mockGetZoneWorkerRoutes } from "./helpers/mock-zone-routes"; import { createFetchResult, msw, @@ -1234,13 +1228,7 @@ describe("deploy", () => { routes: ["example.com/some-route/*"], }); writeWorkerSource(); - mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("example.com", [{ id: "example-com-id" }]); - mockGetZoneWorkerRoutes("example-com-id"); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: ["example.com/some-route/*"] }); await runWrangler("deploy ./index"); }); @@ -1294,43 +1282,6 @@ describe("deploy", () => { writeWorkerSource(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "some-example.com": { - accountId: "some-account-id", - zones: [{ id: "some-example-com-id" }], - }, - "a-boring-website.com": { - accountId: "some-account-id", - zones: [{ id: "a-boring-website-id" }], - }, - "another-boring-website.com": { - accountId: "some-account-id", - zones: [{ id: "another-boring-website-id" }], - }, - "some-zone.com": { - accountId: "some-account-id", - zones: [{ id: "some-zone-id" }], - }, - "example.com": { - accountId: "some-account-id", - zones: [{ id: "example-com-id" }], - }, - "more-examples.com": { - accountId: "some-account-id", - zones: [{ id: "more-examples-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "some-example-com-id": [], - "a-boring-website-id": [], - "another-boring-website-id": [], - "some-zone-id": [], - "example-com-id": [], - "more-examples-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: [ "some-example.com/some-route/*", @@ -1380,8 +1331,6 @@ describe("deploy", () => { writeWorkerSource(); mockUploadWorkerRequest(); mockGetWorkerSubdomain({ enabled: false }); - mockGetZones("owned-zone.com", [{ id: "owned-zone-id-1" }]); - mockGetZoneWorkerRoutes("owned-zone-id-1"); mockPublishRoutesRequest({ routes: [ { @@ -1423,8 +1372,6 @@ describe("deploy", () => { writeWorkerSource(); mockUploadWorkerRequest(); mockGetWorkerSubdomain({ enabled: false }); - mockGetZones("owned-zone.com", [{ id: "owned-zone-id-1" }]); - mockGetZoneWorkerRoutes("owned-zone-id-1"); mockPublishRoutesRequest({ routes: [ { @@ -1489,43 +1436,6 @@ describe("deploy", () => { useServiceEnvironments: true, useOldUploadApi: true, }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "some-example.com": { - accountId: "some-account-id", - zones: [{ id: "some-example-com-id" }], - }, - "a-boring-website.com": { - accountId: "some-account-id", - zones: [{ id: "a-boring-website-id" }], - }, - "another-boring-website.com": { - accountId: "some-account-id", - zones: [{ id: "another-boring-website-id" }], - }, - "some-zone.com": { - accountId: "some-account-id", - zones: [{ id: "some-zone-id" }], - }, - "example.com": { - accountId: "some-account-id", - zones: [{ id: "example-com-id" }], - }, - "more-examples.com": { - accountId: "some-account-id", - zones: [{ id: "more-examples-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "some-example-com-id": [], - "a-boring-website-id": [], - "another-boring-website-id": [], - "some-zone-id": [], - "example-com-id": [], - "more-examples-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: [ "some-example.com/some-route/*", @@ -1589,23 +1499,6 @@ describe("deploy", () => { useServiceEnvironments: false, env: "dev", }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "example.com": { - accountId: "some-account-id", - zones: [{ id: "example-com-id" }], - }, - "dev-example.com": { - accountId: "some-account-id", - zones: [{ id: "dev-example-com-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "example-com-id": [], - "dev-example-com-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: ["dev-example.com/some-route/*"], useServiceEnvironments: false, @@ -1629,23 +1522,6 @@ describe("deploy", () => { expectedType: "esm", env: "dev", }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "example.com": { - accountId: "some-account-id", - zones: [{ id: "example-com-id" }], - }, - "dev-example.com": { - accountId: "some-account-id", - zones: [{ id: "dev-example-com-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "example-com-id": [], - "dev-example-com-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: ["dev-example.com/some-route/*"], env: "dev", @@ -1653,6 +1529,54 @@ describe("deploy", () => { await runWrangler("deploy ./index --env dev --legacy-env false"); }); + it("should surface a helpful error if a route is already assigned to another worker", async () => { + writeWranglerConfig({ + routes: ["example.com/some-route/*"], + }); + writeWorkerSource(); + mockUploadWorkerRequest({ expectedType: "esm" }); + + // Simulate the bulk-routes API failing for an already-assigned route. + msw.use( + http.put( + "*/accounts/:accountId/workers/scripts/:scriptName/routes", + () => { + return HttpResponse.json( + createFetchResult(null, false, [ + { + message: "Route already assigned", + code: 10042, + }, + ]) + ); + }, + { once: true } + ) + ); + + mockGetZones("example.com", [{ id: "example-com-id" }]); + mockGetZoneWorkerRoutes("example-com-id", [ + { pattern: "example.com/some-route/*", script: "other-worker" }, + ]); + + let caught: unknown; + try { + await runWrangler("deploy ./index"); + } catch (error) { + caught = error; + } + + expect(caught).toBeInstanceOf(Error); + expect((caught as Error).message).toContain( + "Can't deploy routes that are assigned to another worker." + ); + expect((caught as Error).message).toContain('"other-worker"'); + expect((caught as Error).message).toContain("example.com/some-route/*"); + expect((caught as Error).message).toContain( + "https://dash.cloudflare.com/some-account-id/workers/overview" + ); + }); + it("should fallback to the Wrangler v1 zone-based API if the bulk-routes API fails", async () => { writeWranglerConfig({ routes: ["example.com/some-route/*"], @@ -1660,8 +1584,7 @@ describe("deploy", () => { writeWorkerSource(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + // These run during zone-based publishRoutes fallback. mockGetZones("example.com", [{ id: "example-com-id" }]); mockGetZoneWorkerRoutes("example-com-id", [ // Simulate that the worker has already been deployed to another route. @@ -1670,7 +1593,6 @@ describe("deploy", () => { script: "test-name", }, ]); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Simulate the bulk-routes API failing with a not authorized error. mockUnauthorizedPublishRoutesRequest(); mockPublishRoutesFallbackRequest({ @@ -1716,8 +1638,7 @@ describe("deploy", () => { writeWorkerSource(); mockUploadWorkerRequest({ env: "staging", expectedType: "esm" }); mockUpdateWorkerSubdomain({ env: "staging", enabled: false }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + // These run during zone-based publishRoutes fallback. mockGetZones("example.com", [{ id: "example-com-id" }]); mockGetZoneWorkerRoutes("example-com-id", [ // Simulate that the worker has already been deployed to another route. @@ -1726,7 +1647,6 @@ describe("deploy", () => { script: "test-name", }, ]); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Simulate the bulk-routes API failing with a not authorized error. mockUnauthorizedPublishRoutesRequest({ env: "staging" }); mockPublishRoutesFallbackRequest({ @@ -1748,11 +1668,6 @@ describe("deploy", () => { writeWorkerSource(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("api.example.com", [{ id: "api-example-com-id" }]); - mockGetZoneWorkerRoutes("api-example-com-id", []); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockCustomDomainsChangesetRequest({}); mockPublishCustomDomainsRequest({ publishFlags: { @@ -1773,11 +1688,6 @@ describe("deploy", () => { writeWorkerSource(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("api.example.com", [{ id: "api-example-com-id" }]); - mockGetZoneWorkerRoutes("api-example-com-id", []); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockCustomDomainsChangesetRequest({ originConflicts: [ { @@ -1823,11 +1733,6 @@ Update them to point to this script instead?`, writeWorkerSource(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("api.example.com", [{ id: "api-example-com-id" }]); - mockGetZoneWorkerRoutes("api-example-com-id", []); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockCustomDomainsChangesetRequest({ dnsRecordConflicts: [ { @@ -1865,11 +1770,6 @@ Update them to point to this script instead?`, writeWorkerSource(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("api.example.com", [{ id: "api-example-com-id" }]); - mockGetZoneWorkerRoutes("api-example-com-id", []); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockCustomDomainsChangesetRequest({ originConflicts: [ { @@ -1962,11 +1862,6 @@ Update them to point to this script instead?`, writeWorkerSource(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("api.example.com", [{ id: "api-example-com-id" }]); - mockGetZoneWorkerRoutes("api-example-com-id", []); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockCustomDomainsChangesetRequest({ originConflicts: [ { @@ -2052,23 +1947,6 @@ Update them to point to this script instead?`, mockSubDomainRequest(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "example.com": { - accountId: "some-account-id", - zones: [{ id: "example-com-id" }], - }, - "api.example.com": { - accountId: "some-account-id", - zones: [{ id: "api-example-com-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "example-com-id": [], - "api-example-com-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockCustomDomainsChangesetRequest({}); mockPublishCustomDomainsRequest({ publishFlags: { @@ -2135,28 +2013,6 @@ Update them to point to this script instead?`, mockSubDomainRequest(); mockUpdateWorkerSubdomain({ enabled: false }); mockUploadWorkerRequest({ expectedType: "esm" }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "config.com": { - accountId: "some-account-id", - zones: [{ id: "config-com-id" }], - }, - "api.example.com": { - accountId: "some-account-id", - zones: [{ id: "api-example-com-id" }], - }, - "cli.com": { - accountId: "some-account-id", - zones: [{ id: "cli-com-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "config-com-id": [], - "api-example-com-id": [], - "cli-com-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockCustomDomainsChangesetRequest({}); mockPublishCustomDomainsRequest({ publishFlags: { @@ -2249,23 +2105,6 @@ Update them to point to this script instead?`, }, expectedType: "none", }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "simple.co.uk": { - accountId: "some-account-id", - zones: [{ id: "simple-co-uk-id" }], - }, - "example.com": { - accountId: "some-account-id", - zones: [{ id: "example-com-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "simple-co-uk-id": [], - "example-com-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: [ // @ts-expect-error - this is what is expected @@ -2354,11 +2193,6 @@ Update them to point to this script instead?`, }, expectedType: "none", }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("example.com", [{ id: "example-com-id" }]); - mockGetZoneWorkerRoutes("example-com-id", []); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: [ { @@ -2426,11 +2260,6 @@ Update them to point to this script instead?`, expectedType: "esm", expectedMainModule: "index.js", }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZones("example.com", [{ id: "example-com-id" }]); - mockGetZoneWorkerRoutes("example-com-id", []); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: [ { @@ -2505,23 +2334,6 @@ Update them to point to this script instead?`, }, expectedType: "none", }); - // These run during route conflict resolution. - // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - mockGetZonesMulti({ - "simple.co.uk": { - accountId: "some-account-id", - zones: [{ id: "simple-co-uk-id" }], - }, - "example.com": { - accountId: "some-account-id", - zones: [{ id: "example-com-id" }], - }, - }); - mockGetZoneWorkerRoutesMulti({ - "simple-co-uk-id": [], - "example-com-id": [], - }); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mockPublishRoutesRequest({ routes: [ // @ts-expect-error - this is what is expected diff --git a/packages/wrangler/src/triggers/deploy.ts b/packages/wrangler/src/triggers/deploy.ts index 01612e218a06..8b665eebed80 100644 --- a/packages/wrangler/src/triggers/deploy.ts +++ b/packages/wrangler/src/triggers/deploy.ts @@ -91,7 +91,7 @@ export default async function triggersDeploy( const uploadMs = Date.now() - start; const deployments: Promise[] = []; - const { wantWorkersDev, workersDevInSync } = await subdomainDeploy( + await subdomainDeploy( props, accountId, scriptName, @@ -102,98 +102,6 @@ export default async function triggersDeploy( props.firstDeploy ); - if (!wantWorkersDev && workersDevInSync && routes.length !== 0) { - // TODO is this true? How does last subdomain status affect route confict?? - // Why would we only need to validate route conflicts if didn't need to - // disable the subdomain deployment? - - // if you get to this point it's because - // you're trying to deploy a worker to a route - // that's already bound to another worker. - // so this thing is about finding workers that have - // bindings to the routes you're trying to deploy to. - // - // the logic is kinda similar (read: duplicated) from publishRoutesFallback, - // except here we know we have a good API token or whatever so we don't need - // to bother with all the error handling tomfoolery. - const routesWithOtherBindings: Record = {}; - - /** - * This queue ensures we limit how many concurrent fetch - * requests we're making to the Zones API. - */ - const queue = new PQueue({ concurrency: 10 }); - const queuePromises: Array> = []; - const zoneRoutesCache = new Map< - string, - Promise> - >(); - - const zoneIdCache = new Map(); - for (const route of routes) { - queuePromises.push( - queue.add(async () => { - const zone = await getZoneForRoute( - config, - { route, accountId }, - zoneIdCache - ); - if (!zone) { - return; - } - - const routePattern = - typeof route === "string" ? route : route.pattern; - - let routesInZone = zoneRoutesCache.get(zone.id); - if (!routesInZone) { - routesInZone = retryOnAPIFailure(() => - fetchListResult<{ - pattern: string; - script: string; - }>(config, `/zones/${zone.id}/workers/routes`) - ); - zoneRoutesCache.set(zone.id, routesInZone); - } - - (await routesInZone).forEach(({ script, pattern }) => { - if (pattern === routePattern && script !== scriptName) { - if (!(script in routesWithOtherBindings)) { - routesWithOtherBindings[script] = []; - } - - routesWithOtherBindings[script].push(pattern); - } - }); - }) - ); - } - // using Promise.all() here instead of queue.onIdle() to ensure - // we actually throw errors that occur within queued promises. - await Promise.all(queuePromises); - - if (Object.keys(routesWithOtherBindings).length > 0) { - let errorMessage = - "Can't deploy routes that are assigned to another worker.\n"; - - for (const worker in routesWithOtherBindings) { - const assignedRoutes = routesWithOtherBindings[worker]; - errorMessage += `"${worker}" is already assigned to routes:\n${assignedRoutes.map( - (r) => ` - ${chalk.underline(r)}\n` - )}`; - } - - const resolution = - "Unassign other workers from the routes you want to deploy to, and then try again."; - const dashHref = chalk.blue.underline( - `https://dash.cloudflare.com/${accountId}/workers/overview` - ); - const dashLink = `Visit ${dashHref} to unassign a worker from a route.`; - - throw new UserError(`${errorMessage}\n${resolution}\n${dashLink}`); - } - } - // Update routing table for the script. if (routesOnly.length > 0) { deployments.push( @@ -202,15 +110,42 @@ export default async function triggersDeploy( scriptName, useServiceEnvironments, accountId, - }).then(() => { - if (routesOnly.length > 10) { - return routesOnly - .slice(0, 9) - .map((route) => renderRoute(route)) - .concat([`...and ${routesOnly.length - 10} more routes`]); - } - return routesOnly.map((route) => renderRoute(route)); }) + .then(() => { + if (routesOnly.length > 10) { + return routesOnly + .slice(0, 9) + .map((route) => renderRoute(route)) + .concat([`...and ${routesOnly.length - 10} more routes`]); + } + return routesOnly.map((route) => renderRoute(route)); + }) + .catch(async (error) => { + let routesWithOtherBindings: Record | undefined; + try { + routesWithOtherBindings = await getRoutesWithOtherBindings({ + config, + accountId, + routes: routesOnly, + scriptName, + }); + } catch { + // If we can't determine conflicts (e.g. permission issues), fall back to + // the original error from the bulk routes API. + } + + if ( + routesWithOtherBindings && + Object.keys(routesWithOtherBindings).length > 0 + ) { + throw createRoutesWithOtherBindingsError( + routesWithOtherBindings, + accountId + ); + } + + throw error; + }) ); } @@ -544,3 +479,96 @@ async function subdomainDeploy( previewsInSync: before.previews_enabled === after.previews_enabled, }; } + +async function getRoutesWithOtherBindings({ + config, + accountId, + routes, + scriptName, +}: { + config: Config; + accountId: string; + routes: Route[]; + scriptName: string; +}): Promise> { + const routesWithOtherBindings: Record = {}; + + /** + * This queue ensures we limit how many concurrent fetch + * requests we're making to the Zones API. + */ + const queue = new PQueue({ concurrency: 10 }); + const queuePromises: Array> = []; + const zoneRoutesCache = new Map< + string, + Promise> + >(); + + const zoneIdCache = new Map>(); + for (const route of routes) { + queuePromises.push( + queue.add(async () => { + const zone = await getZoneForRoute( + config, + { route, accountId }, + zoneIdCache + ); + if (!zone) { + return; + } + + const routePattern = typeof route === "string" ? route : route.pattern; + + let routesInZone = zoneRoutesCache.get(zone.id); + if (!routesInZone) { + routesInZone = retryOnAPIFailure(() => + fetchListResult<{ pattern: string; script: string }>( + config, + `/zones/${zone.id}/workers/routes` + ) + ); + zoneRoutesCache.set(zone.id, routesInZone); + } + + (await routesInZone).forEach(({ script, pattern }) => { + if (pattern === routePattern && script !== scriptName) { + if (!(script in routesWithOtherBindings)) { + routesWithOtherBindings[script] = []; + } + + routesWithOtherBindings[script].push(pattern); + } + }); + }) + ); + } + // using Promise.all() here instead of queue.onIdle() to ensure + // we actually throw errors that occur within queued promises. + await Promise.all(queuePromises); + + return routesWithOtherBindings; +} + +function createRoutesWithOtherBindingsError( + routesWithOtherBindings: Record, + accountId: string +) { + let errorMessage = + "Can't deploy routes that are assigned to another worker.\n"; + + for (const worker in routesWithOtherBindings) { + const assignedRoutes = routesWithOtherBindings[worker]; + errorMessage += `"${worker}" is already assigned to routes:\n${assignedRoutes + .map((r) => ` - ${chalk.underline(r)}\n`) + .join("")}`; + } + + const resolution = + "Unassign other workers from the routes you want to deploy to, and then try again."; + const dashHref = chalk.blue.underline( + `https://dash.cloudflare.com/${accountId}/workers/overview` + ); + const dashLink = `Visit ${dashHref} to unassign a worker from a route.`; + + return new UserError(`${errorMessage}\n${resolution}\n${dashLink}`); +}