From a25101bfb41822868ba939c51d953ddf050cf46a Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:01:53 +0000 Subject: [PATCH 1/3] quarantine solid c3 tests (#12831) --- packages/create-cloudflare/e2e/tests/frameworks/test-config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts index 251cc94b1bd5..084a0cd5ea68 100644 --- a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts +++ b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts @@ -455,6 +455,7 @@ function getFrameworkTestConfig(pm: string): NamedFrameworkTestConfig[] { }, { name: "solid", + quarantine: true, promptHandlers: [ { matcher: /Which template would you like to use/, @@ -810,6 +811,7 @@ function getExperimentalFrameworkTestConfig( }, { name: "solid", + quarantine: true, promptHandlers: [ { matcher: /Which template would you like to use/, From ba18c0208a000a1f686780ed153107cb5f4a4afe Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 10 Mar 2026 17:50:49 +0000 Subject: [PATCH 2/3] [wrangler] Make worker deletion in e2e test cleanup best-effort (#12832) --- packages/wrangler/e2e/deploy.test.ts | 2 +- packages/wrangler/e2e/deployments.test.ts | 14 ++++++----- .../wrangler/e2e/helpers/e2e-wrangler-test.ts | 24 ++++++++++++++++++- packages/wrangler/e2e/provision.test.ts | 2 +- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/wrangler/e2e/deploy.test.ts b/packages/wrangler/e2e/deploy.test.ts index e5b67e5ee74a..55ba7b5c8064 100644 --- a/packages/wrangler/e2e/deploy.test.ts +++ b/packages/wrangler/e2e/deploy.test.ts @@ -44,7 +44,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("deploy", { timeout: TIMEOUT }, () => { }); afterAll(async () => { - await helper.run(`wrangler delete`); + await helper.bestEffortRun(`wrangler delete`); }); it("omit subdomain warnings on 1st deploy", async () => { diff --git a/packages/wrangler/e2e/deployments.test.ts b/packages/wrangler/e2e/deployments.test.ts index dedb08725b39..433eb338beec 100644 --- a/packages/wrangler/e2e/deployments.test.ts +++ b/packages/wrangler/e2e/deployments.test.ts @@ -32,7 +32,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( afterAll(async () => { // clean up user Worker after all tests - await helper.run(`wrangler delete`); + await helper.bestEffortRun(`wrangler delete`); }); it("deploys a Worker", async () => { @@ -267,7 +267,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { describe("Workers", () => { afterEach(async () => { // clean up user Worker after each test - await helper.run(`wrangler delete`); + await helper.bestEffortRun(`wrangler delete`); }); it("deploys a Workers + Assets project with assets only", async () => { @@ -583,8 +583,10 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("Workers + Assets deployment", () => { afterEach(async () => { // clean up dispatch Worker - await helper.run(`wrangler delete -c dispatch-worker/wrangler.toml`); - await helper.run( + await helper.bestEffortRun( + `wrangler delete -c dispatch-worker/wrangler.toml` + ); + await helper.bestEffortRun( `wrangler dispatch-namespace delete ${dispatchNamespaceName}` ); }); @@ -937,8 +939,8 @@ describe.skipIf(skipContainersTest)("containers", () => { afterAll(async () => { // clean up user Worker after each test - const deleteWorker = helper.run(`wrangler delete`); - const deleteContainer = helper.run( + const deleteWorker = helper.bestEffortRun(`wrangler delete`); + const deleteContainer = helper.bestEffortRun( `wrangler containers delete ${applicationId}` ); await Promise.allSettled([deleteWorker, deleteContainer]); diff --git a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts index 3a28d7ad305b..f924b521b36d 100644 --- a/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts +++ b/packages/wrangler/e2e/helpers/e2e-wrangler-test.ts @@ -93,6 +93,28 @@ export class WranglerE2ETestHelper { return runWrangler(wranglerCommand, { cwd, ...options }); } + /** + * Run a Wrangler command as best-effort cleanup. + * + * Uses a short timeout (default 5s) and swallows errors, so that flaky or + * slow backend responses during resource deletion don't cause test hooks to + * time out and mask real test failures. + */ + async bestEffortRun( + wranglerCommand: string, + { timeout = 5_000, ...options }: WranglerCommandOptions = {} + ) { + try { + return await this.run(wranglerCommand, { timeout, ...options }); + } catch (e) { + console.warn( + `Best-effort cleanup "${wranglerCommand}" failed:`, + e instanceof Error ? e.message : e + ); + return undefined; + } + } + /** Create a KV namespace and clean it up during tear-down. */ async kv(isLocal: boolean) { const name = generateResourceName("kv" + Date.now()).replaceAll("-", "_"); @@ -311,7 +333,7 @@ export class WranglerE2ETestHelper { ); const cleanup = async () => { - await this.run(`wrangler delete --name ${workerName} --force`); + await this.bestEffortRun(`wrangler delete --name ${workerName} --force`); }; if (cleanOnTestFinished) { diff --git a/packages/wrangler/e2e/provision.test.ts b/packages/wrangler/e2e/provision.test.ts index 749da4ce58b7..b2362d14ec9d 100644 --- a/packages/wrangler/e2e/provision.test.ts +++ b/packages/wrangler/e2e/provision.test.ts @@ -244,7 +244,7 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)( await Promise.allSettled([ helper.run(`wrangler r2 bucket delete ${workerName}-r2`), helper.run(`wrangler d1 delete ${workerName}-d1 -y`), - helper.run(`wrangler delete`), + helper.bestEffortRun(`wrangler delete`), helper.run(`wrangler kv namespace delete --namespace-id ${kvId}`), helper.run(`wrangler kv namespace delete --namespace-id ${kvId2}`), ]); From b8c33f5509a202cf4d4ebe5bd38c5705dffd9346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Somhairle=20MacLe=C3=B2id?= Date: Tue, 10 Mar 2026 15:07:43 -0400 Subject: [PATCH 3/3] Make remote dev `exchange_url` optional (#12771) --- .changeset/optional-exchange-url.md | 7 + .changeset/remove-prewarm-inspector.md | 11 + .../src/index.ts | 108 +------- .../tests/index.test.ts | 66 +---- .../playground-preview-worker/src/index.ts | 17 -- .../playground-preview-worker/src/realish.ts | 40 +-- .../playground-preview-worker/src/user.do.ts | 41 --- .../src/QuickEditor/useDraftWorker.ts | 1 - packages/wrangler/e2e/dev.test.ts | 6 +- packages/wrangler/e2e/startWorker.test.ts | 241 +++++++++--------- .../RemoteRuntimeController.test.ts | 4 +- .../dev/remote-bindings-errors.test.ts | 3 +- packages/wrangler/src/api/dev.ts | 1 - .../api/startDevWorker/ConfigController.ts | 14 +- .../src/api/startDevWorker/ProxyController.ts | 18 +- .../startDevWorker/RemoteRuntimeController.ts | 43 ++-- .../wrangler/src/api/startDevWorker/types.ts | 4 +- packages/wrangler/src/dev.ts | 8 - .../wrangler/src/dev/create-worker-preview.ts | 229 +++++++---------- packages/wrangler/src/dev/hotkeys.ts | 5 +- packages/wrangler/src/dev/start-dev.ts | 3 - packages/wrangler/src/pages/dev.ts | 1 - packages/wrangler/src/routes.ts | 10 +- 23 files changed, 311 insertions(+), 570 deletions(-) create mode 100644 .changeset/optional-exchange-url.md create mode 100644 .changeset/remove-prewarm-inspector.md diff --git a/.changeset/optional-exchange-url.md b/.changeset/optional-exchange-url.md new file mode 100644 index 000000000000..af2cfb481094 --- /dev/null +++ b/.changeset/optional-exchange-url.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Make remote dev `exchange_url` optional + +The edge-preview API's `exchange_url` is now treated as optional. When unavailable or when the exchange fails, the initial token from the API response is used directly. The `prewarm` step and `inspector_websocket` have been removed from the remote dev flow in favour of `tail_url` for live logs. diff --git a/.changeset/remove-prewarm-inspector.md b/.changeset/remove-prewarm-inspector.md new file mode 100644 index 000000000000..de8867a49016 --- /dev/null +++ b/.changeset/remove-prewarm-inspector.md @@ -0,0 +1,11 @@ +--- +"@cloudflare/edge-preview-authenticated-proxy": minor +"@cloudflare/playground-preview-worker": minor +"@cloudflare/workers-playground": minor +--- + +Remove prewarm, inspector_websocket, and exchange proxy from preview flow + +The preview session exchange endpoint (`/exchange`) has been removed from the edge-preview-authenticated-proxy — it has been unused since the dash started fetching the exchange URL directly (DEVX-979). The `prewarm` parameter is no longer required or accepted by the `.update-preview-token` endpoint. + +The playground preview worker now treats `exchange_url` as optional, falling back to the initial token from the edge-preview API when exchange is unavailable. Inspector websocket proxying and prewarm have been removed in favour of using `tail_url` for live logs. diff --git a/packages/edge-preview-authenticated-proxy/src/index.ts b/packages/edge-preview-authenticated-proxy/src/index.ts index a3e6bc7baa73..03d960b71d88 100644 --- a/packages/edge-preview-authenticated-proxy/src/index.ts +++ b/packages/edge-preview-authenticated-proxy/src/index.ts @@ -33,29 +33,9 @@ class HttpError extends Error { } } -class NoExchangeUrl extends HttpError { - constructor() { - super("No exchange_url provided", 400, false); - } -} - -class ExchangeFailed extends HttpError { - constructor( - readonly url: string, - readonly exchangeStatus: number, - readonly body: string - ) { - super("Exchange failed", 400, true); - } - - get data(): { url: string; status: number; body: string } { - return { url: this.url, status: this.exchangeStatus, body: this.body }; - } -} - class TokenUpdateFailed extends HttpError { constructor() { - super("Provide token, prewarmUrl and remote", 400, false); + super("Provide token and remote", 400, false); } } @@ -101,14 +81,6 @@ function switchRemote(url: URL, remote: string) { return workerUrl; } -function isTokenExchangeRequest(request: Request, url: URL, env: Env) { - return ( - request.method === "POST" && - url.hostname === env.PREVIEW && - url.pathname === "/exchange" - ); -} - function isPreviewUpdateRequest(request: Request, url: URL, env: Env) { return ( request.method === "GET" && @@ -121,19 +93,11 @@ function isRawHttpRequest(url: URL, env: Env) { return url.hostname.endsWith(env.RAW_HTTP); } -async function handleRequest( - request: Request, - env: Env, - ctx: ExecutionContext -) { +async function handleRequest(request: Request, env: Env) { const url = new URL(request.url); - if (isTokenExchangeRequest(request, url, env)) { - return handleTokenExchange(url); - } - if (isPreviewUpdateRequest(request, url, env)) { - return updatePreviewToken(url, env, ctx); + return updatePreviewToken(url, env); } if (isRawHttpRequest(url, env)) { @@ -295,27 +259,15 @@ async function handleRawHttp(request: Request, url: URL) { * It will redirect to the suffix provide, setting a cookie with the `token` and `remote` * for future use. */ -async function updatePreviewToken(url: URL, env: Env, ctx: ExecutionContext) { +async function updatePreviewToken(url: URL, env: Env) { const token = url.searchParams.get("token"); - const prewarmUrl = url.searchParams.get("prewarm"); const remote = url.searchParams.get("remote"); - // return Response.json([...url.searchParams.entries()]); - if (!token || !prewarmUrl || !remote) { + if (!token || !remote) { throw new TokenUpdateFailed(); } - assertValidURL(prewarmUrl); assertValidURL(remote); - ctx.waitUntil( - fetch(prewarmUrl, { - method: "POST", - headers: { - "cf-workers-preview-token": token, - }, - }) - ); - // The token can sometimes be too large for a cookie (4096 bytes). // Store the token in KV, and allow lookups @@ -342,54 +294,6 @@ async function updatePreviewToken(url: URL, env: Env, ctx: ExecutionContext) { }); } -/** - * Request the preview session associated with a given exchange_url - * exchange_url comes from an authenticated core API call made in the client - * It doesn't have CORS set up, so needs to be proxied - */ -async function handleTokenExchange(url: URL) { - const exchangeUrl = url.searchParams.get("exchange_url"); - if (!exchangeUrl) { - throw new NoExchangeUrl(); - } - assertValidURL(exchangeUrl); - const exchangeRes = await fetch(exchangeUrl); - if (exchangeRes.status !== 200) { - const exchange = new URL(exchangeUrl); - // Clear sensitive token - exchange.search = ""; - - throw new ExchangeFailed( - exchange.href, - exchangeRes.status, - await exchangeRes.text() - ); - } - const session = await exchangeRes.json<{ - prewarm: string; - token: string; - }>(); - if ( - typeof session.token !== "string" || - typeof session.prewarm !== "string" - ) { - const exchange = new URL(exchangeUrl); - // Clear sensitive token - exchange.search = ""; - throw new ExchangeFailed( - exchange.href, - exchangeRes.status, - JSON.stringify(session) - ); - } - return Response.json(session, { - headers: { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "POST", - }, - }); -} - // No ecosystem routers support hostname matching 😥 export default { async fetch( @@ -429,7 +333,7 @@ export default { }); try { - return await handleRequest(request, env, ctx); + return await handleRequest(request, env); } catch (e) { console.error(e); if (e instanceof HttpError) { diff --git a/packages/edge-preview-authenticated-proxy/tests/index.test.ts b/packages/edge-preview-authenticated-proxy/tests/index.test.ts index b52f51dbb191..abf20deca1fb 100644 --- a/packages/edge-preview-authenticated-proxy/tests/index.test.ts +++ b/packages/edge-preview-authenticated-proxy/tests/index.test.ts @@ -29,12 +29,6 @@ function createMockFetchImplementation() { return new Response("BAD", { status: 500 }); } - if (url.pathname === "/exchange") { - return Response.json({ - token: "TEST_TOKEN", - prewarm: "TEST_PREWARM", - }); - } if (url.pathname === "/redirect") { // Use manual redirect to avoid trailing slash being added return new Response(null, { @@ -61,10 +55,6 @@ function createMockFetchImplementation() { headers.append("Set-Cookie", "bar=2"); return new Response(undefined, { headers }); } - if (url.pathname === "/prewarm") { - return new Response("OK"); - } - return Response.json( { url: request.url, @@ -86,36 +76,6 @@ afterEach(() => { }); describe("Preview Worker", () => { - it("should obtain token from exchange_url", async ({ expect }) => { - const resp = await SELF.fetch( - `https://preview.devprod.cloudflare.dev/exchange?exchange_url=${encodeURIComponent( - `${MOCK_REMOTE_URL}/exchange` - )}`, - { - method: "POST", - } - ); - const text = await resp.json(); - expect(text).toMatchInlineSnapshot( - ` - { - "prewarm": "TEST_PREWARM", - "token": "TEST_TOKEN", - } - ` - ); - }); - it("should reject invalid exchange_url", async ({ expect }) => { - vi.spyOn(console, "error").mockImplementation(() => {}); - const resp = await SELF.fetch( - `https://preview.devprod.cloudflare.dev/exchange?exchange_url=not_an_exchange_url`, - { method: "POST" } - ); - expect(resp.status).toBe(400); - expect(await resp.text()).toMatchInlineSnapshot( - `"{"error":"Error","message":"Invalid URL"}"` - ); - }); it("should allow tokens > 4096 bytes", async ({ expect }) => { // 4096 is the size limit for cookies const token = randomBytes(4096).toString("hex"); @@ -124,8 +84,6 @@ describe("Preview Worker", () => { let resp = await SELF.fetch( `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=${encodeURIComponent( token - )}&prewarm=${encodeURIComponent( - `${MOCK_REMOTE_URL}/prewarm` )}&remote=${encodeURIComponent( MOCK_REMOTE_URL )}&suffix=${encodeURIComponent("/hello?world")}`, @@ -164,9 +122,7 @@ describe("Preview Worker", () => { }); it("should be redirected with cookie", async ({ expect }) => { const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=${encodeURIComponent( - `${MOCK_REMOTE_URL}/prewarm` - )}&remote=${encodeURIComponent( + `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( MOCK_REMOTE_URL )}&suffix=${encodeURIComponent("/hello?world")}`, { @@ -186,9 +142,7 @@ describe("Preview Worker", () => { async function getToken() { const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=${encodeURIComponent( - `${MOCK_REMOTE_URL}/prewarm` - )}&remote=${encodeURIComponent( + `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=${encodeURIComponent( MOCK_REMOTE_URL )}&suffix=${encodeURIComponent("/hello?world")}`, { @@ -198,24 +152,10 @@ describe("Preview Worker", () => { ); return (resp.headers.get("set-cookie") ?? "").split(";")[0].split("=")[1]; } - it("should reject invalid prewarm url", async ({ expect }) => { - vi.spyOn(console, "error").mockImplementation(() => {}); - const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=not_a_prewarm_url&remote=${encodeURIComponent( - MOCK_REMOTE_URL - )}&suffix=${encodeURIComponent("/hello?world")}` - ); - expect(resp.status).toBe(400); - expect(await resp.text()).toMatchInlineSnapshot( - `"{"error":"Error","message":"Invalid URL"}"` - ); - }); it("should reject invalid remote url", async ({ expect }) => { vi.spyOn(console, "error").mockImplementation(() => {}); const resp = await SELF.fetch( - `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&prewarm=${encodeURIComponent( - `${MOCK_REMOTE_URL}/prewarm` - )}&remote=not_a_remote_url&suffix=${encodeURIComponent("/hello?world")}` + `https://random-data.preview.devprod.cloudflare.dev/.update-preview-token?token=TEST_TOKEN&remote=not_a_remote_url&suffix=${encodeURIComponent("/hello?world")}` ); expect(resp.status).toBe(400); expect(await resp.text()).toMatchInlineSnapshot( diff --git a/packages/playground-preview-worker/src/index.ts b/packages/playground-preview-worker/src/index.ts index a7d25151abf6..1e6c24f1a24f 100644 --- a/packages/playground-preview-worker/src/index.ts +++ b/packages/playground-preview-worker/src/index.ts @@ -200,23 +200,6 @@ app.post(`${rootDomain}/api/worker`, async (c) => { }); }); -app.get(`${rootDomain}/api/inspector`, async (c) => { - const url = new URL(c.req.url); - const userId = url.searchParams.get("user"); - if (!userId) { - throw new PreviewRequestFailed("", false); - } - let userObjectId: DurableObjectId; - try { - userObjectId = c.env.UserSession.idFromString(userId); - } catch { - throw new PreviewRequestFailed(userId, false); - } - const userObject = c.env.UserSession.get(userObjectId); - - return userObject.fetch(c.req.raw); -}); - /** * Given a preview session, the client should obtain a specific preview token * This endpoint takes in the URL parameters: diff --git a/packages/playground-preview-worker/src/realish.ts b/packages/playground-preview-worker/src/realish.ts index 2c65a6136cd2..5c893d4278d8 100644 --- a/packages/playground-preview-worker/src/realish.ts +++ b/packages/playground-preview-worker/src/realish.ts @@ -21,21 +21,13 @@ const APIResponse = (resultSchema: T) => const PreviewSession = APIResponse( z.object({ - exchange_url: z.string(), + exchange_url: z.string().optional(), token: z.string(), }) ); type PreviewSession = z.infer; -const UploadToken = z.object({ - token: z.string(), - inspector_websocket: z.string(), - prewarm: z.string(), -}); - -type UploadToken = z.infer; - const UploadResult = APIResponse( z.object({ preview_token: z.string(), @@ -45,7 +37,7 @@ const UploadResult = APIResponse( export type UploadResult = z.infer; export type RealishPreviewConfig = { - uploadConfigToken: UploadToken; + uploadConfigToken: string; previewSession: PreviewSession["result"]; }; @@ -64,7 +56,7 @@ async function initialiseSubdomainPreview( accountId: string, apiToken: string ): Promise<{ - exchange_url: string; + exchange_url?: string; token: string; }> { const response = await cloudflareFetch( @@ -82,10 +74,20 @@ async function initialiseSubdomainPreview( return session.result; } -async function exchangeToken(url: string): Promise { - const response = await fetch(url); - const json = await response.json(); - return UploadToken.parse(json); +async function tryExpandToken(exchangeUrl: string): Promise { + try { + const response = await fetch(exchangeUrl); + if (!response.ok) { + return null; + } + const json = (await response.json()) as { token?: string }; + if (typeof json?.token !== "string") { + return null; + } + return json.token; + } catch { + return null; + } } export async function setupTokens( @@ -93,7 +95,11 @@ export async function setupTokens( apiToken: string ): Promise { const previewSession = await initialiseSubdomainPreview(accountId, apiToken); - const uploadConfigToken = await exchangeToken(previewSession.exchange_url); + const uploadConfigToken = previewSession.exchange_url + ? (await tryExpandToken(previewSession.exchange_url)) ?? + previewSession.token + : previewSession.token; + return { previewSession, uploadConfigToken, @@ -114,7 +120,7 @@ export async function doUpload( method: "POST", headers: { "User-Agent": "workers-playground", - "cf-preview-upload-config-token": config.uploadConfigToken?.token ?? "", + "cf-preview-upload-config-token": config.uploadConfigToken ?? "", }, body: worker, } diff --git a/packages/playground-preview-worker/src/user.do.ts b/packages/playground-preview-worker/src/user.do.ts index d581817ec096..18922ecf1832 100644 --- a/packages/playground-preview-worker/src/user.do.ts +++ b/packages/playground-preview-worker/src/user.do.ts @@ -1,5 +1,4 @@ import assert from "node:assert"; -import { Buffer } from "node:buffer"; import z from "zod"; import { BadUpload, ServiceWorkerNotSupported, WorkerTimeout } from "./errors"; import { constructMiddleware } from "./inject-middleware"; @@ -7,13 +6,6 @@ import { doUpload, setupTokens } from "./realish"; import { handleException, setupSentry } from "./sentry"; import type { RealishPreviewConfig, UploadResult } from "./realish"; -const encoder = new TextEncoder(); - -async function hash(text: string) { - const digest = await crypto.subtle.digest("SHA-256", encoder.encode(text)); - return Buffer.from(digest).toString("hex"); -} - function switchRemote(url: URL, remote: string) { const workerUrl = new URL(url); const remoteUrl = new URL(remote); @@ -46,7 +38,6 @@ export class UserSession { config: RealishPreviewConfig | undefined; previewToken: string | undefined; tailUrl: string | undefined; - inspectorUrl: string | undefined; workerName!: string; constructor( private state: DurableObjectState, @@ -64,7 +55,6 @@ export class UserSession { this.previewToken = await this.state.storage.get("previewToken"); this.tailUrl = await this.state.storage.get("tailUrl"); - this.inspectorUrl = await this.state.storage.get("inspectorUrl"); }); } async refreshTokens() { @@ -103,39 +93,14 @@ export class UserSession { ); } - const inspector = new URL( - this.config.uploadConfigToken.inspector_websocket - ); - inspector.searchParams.append( - "cf_workers_preview_token", - this.config.uploadConfigToken.token - ); - inspector.protocol = "https:"; - // Fire and forget - void fetch(this.config.uploadConfigToken.prewarm, { - method: "POST", - headers: { - "cf-workers-preview-token": uploadResult.result.preview_token, - }, - }); this.previewToken = uploadResult.result.preview_token; this.tailUrl = uploadResult.result.tail_url; - this.inspectorUrl = inspector.href; await this.state.storage.put("previewToken", this.previewToken); await this.state.storage.put("tailUrl", this.tailUrl); - await this.state.storage.put("inspectorUrl", this.inspectorUrl); } async handleRequest(request: Request) { - const url = new URL(request.url); - - // This is an inspector request. Forward to the correct inspector URL - if (request.headers.get("Upgrade") && url.pathname === "/api/inspector") { - assert(this.inspectorUrl !== undefined); - return fetch(this.inspectorUrl, request); - } - // This is a request to run the user-worker. Forward, adding the correct authentication headers if (request.headers.has("cf-run-user-worker")) { assert(this.previewToken !== undefined); @@ -243,15 +208,9 @@ export class UserSession { await this.uploadWorker(this.workerName, worker); - assert(this.inspectorUrl !== undefined); assert(this.tailUrl !== undefined); return Response.json({ - // Include a hash of the inspector URL so as to ensure the client will reconnect - // when the inspector URL has changed (because of an updated preview session) - inspector: `/api/inspector?user=${userSession}&h=${await hash( - this.inspectorUrl - )}`, tail: this.tailUrl, preview: userSession, }); diff --git a/packages/workers-playground/src/QuickEditor/useDraftWorker.ts b/packages/workers-playground/src/QuickEditor/useDraftWorker.ts index 169385906f7d..177a3bfa775f 100644 --- a/packages/workers-playground/src/QuickEditor/useDraftWorker.ts +++ b/packages/workers-playground/src/QuickEditor/useDraftWorker.ts @@ -11,7 +11,6 @@ const decoder = new TextDecoder(); const encoder = new TextEncoder(); export const DeployPlaygroundWorkerResponse = eg.union([ eg.object({ - inspector: eg.string, preview: eg.string, tail: eg.string, }), diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index 1230a7f0956a..950ce9d5029c 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -1324,10 +1324,10 @@ describe.skipIf(CLOUDFLARE_ACCOUNT_ID !== "8d783f274e1f82dc46744c297b015a2f")( `, }); const worker = helper.runLongLived("wrangler dev --remote"); + const { url } = await worker.waitForReady(); - await worker.readUntil( - /Could not access `not-a-domain.testing.devprod.cloudflare.dev`. Make sure the domain is set up to be proxied by Cloudflare/ - ); + await fetchText(url); + await worker.readUntil(/ERROR/); }); } ); diff --git a/packages/wrangler/e2e/startWorker.test.ts b/packages/wrangler/e2e/startWorker.test.ts index 4e651f5944dc..4cd8aa4fa332 100644 --- a/packages/wrangler/e2e/startWorker.test.ts +++ b/packages/wrangler/e2e/startWorker.test.ts @@ -101,8 +101,10 @@ describe("DevEnv", { sequential: true }, () => { }); }); - it("InspectorProxyWorker discovery endpoints + devtools websocket connection", async () => { - const script = dedent` + it.skipIf(remote)( + "InspectorProxyWorker discovery endpoints + devtools websocket connection", + async () => { + const script = dedent` export default { fetch() { console.log('Inside mock user worker'); @@ -112,111 +114,117 @@ describe("DevEnv", { sequential: true }, () => { } `; - await helper.seed({ - "src/index.ts": script, - }); + await helper.seed({ + "src/index.ts": script, + }); - const worker = await startWorker({ - name: "test-worker", - entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + const worker = await startWorker({ + name: "test-worker", + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), - dev: { - remote, - server: { port: 0 }, - inspector: { port: 0 }, - }, - }); - onTestFinished(worker?.dispose); + dev: { + remote, + server: { port: 0 }, + inspector: { port: 0 }, + }, + }); + onTestFinished(worker?.dispose); - const inspectorUrl = await worker.inspectorUrl; - assert(inspectorUrl, "missing inspectorUrl"); - const res = await undici.fetch(`http://${inspectorUrl.host}/json`); + const inspectorUrl = await worker.inspectorUrl; + assert(inspectorUrl, "missing inspectorUrl"); + const res = await undici.fetch(`http://${inspectorUrl.host}/json`); - await expect(res.json()).resolves.toBeInstanceOf(Array); + await expect(res.json()).resolves.toBeInstanceOf(Array); - assert(inspectorUrl, "missing inspectorUrl"); - const ws = new WebSocket(inspectorUrl.href); - const openPromise = events.once(ws, "open"); + assert(inspectorUrl, "missing inspectorUrl"); + const ws = new WebSocket(inspectorUrl.href); + const openPromise = events.once(ws, "open"); - const consoleApiMessages: DevToolsEvent<"Runtime.consoleAPICalled">[] = - collectMessagesContaining(ws, "Runtime.consoleAPICalled"); - const executionContextCreatedPromise = waitForMessageContaining( - ws, - "Runtime.executionContextCreated" - ); + const consoleApiMessages: DevToolsEvent<"Runtime.consoleAPICalled">[] = + collectMessagesContaining(ws, "Runtime.consoleAPICalled"); + const executionContextCreatedPromise = waitForMessageContaining( + ws, + "Runtime.executionContextCreated" + ); - await openPromise; - await worker.fetch("http://dummy"); + await openPromise; + await worker.fetch("http://dummy"); - await expect(executionContextCreatedPromise).resolves.toMatchObject({ - method: "Runtime.executionContextCreated", - params: { - context: { id: expect.any(Number) }, - }, - }); - await waitFor(() => { - expect(consoleApiMessages).toContainMatchingObject({ - method: "Runtime.consoleAPICalled", - params: expect.objectContaining({ - args: [{ type: "string", value: "Inside mock user worker" }], - }), + await expect(executionContextCreatedPromise).resolves.toMatchObject({ + method: "Runtime.executionContextCreated", + params: { + context: { id: expect.any(Number) }, + }, + }); + await waitFor(() => { + expect(consoleApiMessages).toContainMatchingObject({ + method: "Runtime.consoleAPICalled", + params: expect.objectContaining({ + args: [{ type: "string", value: "Inside mock user worker" }], + }), + }); }); - }); - // Ensure execution contexts cleared on reload - const executionContextClearedPromise = waitForMessageContaining( - ws, - "Runtime.executionContextsCleared" - ); - await helper.seed({ - "src/index.ts": script.replace("body:1", "body:2"), - }); + // Ensure execution contexts cleared on reload + const executionContextClearedPromise = waitForMessageContaining( + ws, + "Runtime.executionContextsCleared" + ); + await helper.seed({ + "src/index.ts": script.replace("body:1", "body:2"), + }); - await executionContextClearedPromise; - }); + await executionContextClearedPromise; + } + ); - it("InspectorProxyWorker rejects unauthorised requests", async () => { - await helper.seed({ - "src/index.ts": dedent` + it.skipIf(remote)( + "InspectorProxyWorker rejects unauthorised requests", + async () => { + await helper.seed({ + "src/index.ts": dedent` export default { fetch() { return new Response("body:1"); } } `, - }); + }); - const worker = await startWorker({ - name: "test-worker", - entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + const worker = await startWorker({ + name: "test-worker", + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), - dev: { - remote, - server: { port: 0 }, - inspector: { port: 0 }, - }, - }); - onTestFinished(worker?.dispose); + dev: { + remote, + server: { port: 0 }, + inspector: { port: 0 }, + }, + }); + onTestFinished(worker?.dispose); - const inspectorUrl = await worker.inspectorUrl; - assert(inspectorUrl); + const inspectorUrl = await worker.inspectorUrl; + assert(inspectorUrl); - assert(inspectorUrl, "missing inspectorUrl"); - let ws = new WebSocket(inspectorUrl.href, { - setHost: false, - headers: { Host: "example.com" }, - }); + assert(inspectorUrl, "missing inspectorUrl"); + let ws = new WebSocket(inspectorUrl.href, { + setHost: false, + headers: { Host: "example.com" }, + }); - let openPromise = events.once(ws, "open"); - await expect(openPromise).rejects.toThrow("Unexpected server response"); + let openPromise = events.once(ws, "open"); + await expect(openPromise).rejects.toThrow("Unexpected server response"); - // Check validates `Origin` header - assert(inspectorUrl, "missing inspectorUrl"); - ws = new WebSocket(inspectorUrl.href, { origin: "https://example.com" }); - openPromise = events.once(ws, "open"); - await expect(openPromise).rejects.toThrow("Unexpected server response"); - ws.close(); - }); + // Check validates `Origin` header + assert(inspectorUrl, "missing inspectorUrl"); + ws = new WebSocket(inspectorUrl.href, { + origin: "https://example.com", + }); + openPromise = events.once(ws, "open"); + await expect(openPromise).rejects.toThrow("Unexpected server response"); + ws.close(); + } + ); // Regression test for https://github.com/cloudflare/workers-sdk/issues/5297 // The runtime inspector can send messages larger than 1MB limit websocket message permitted by UserWorkers. @@ -225,13 +233,15 @@ describe("DevEnv", { sequential: true }, () => { // Connecting devtools directly to the inspector would work fine, but we proxy the inspector messages // through a worker (InspectorProxyWorker) which hits the limit (without the fix, compatibilityFlags:["increase_websocket_message_size"]) // By logging a large string we can verify that the inspector messages are being proxied successfully. - it("InspectorProxyWorker can proxy messages > 1MB", async () => { - vi.spyOn(console, "info").mockImplementation(() => {}); - vi.spyOn(console, "log").mockImplementation(() => {}); + it.skipIf(remote)( + "InspectorProxyWorker can proxy messages > 1MB", + async () => { + vi.spyOn(console, "info").mockImplementation(() => {}); + vi.spyOn(console, "log").mockImplementation(() => {}); - const LARGE_STRING = "This is a large string" + "z".repeat(2 ** 20); + const LARGE_STRING = "This is a large string" + "z".repeat(2 ** 20); - const script = dedent` + const script = dedent` export default { fetch() { console.log("${LARGE_STRING}"); @@ -241,40 +251,41 @@ describe("DevEnv", { sequential: true }, () => { } `; - await helper.seed({ - "src/index.ts": script, - }); + await helper.seed({ + "src/index.ts": script, + }); - const worker = await startWorker({ - name: "test-worker", - entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), + const worker = await startWorker({ + name: "test-worker", + entrypoint: path.resolve(helper.tmpPath, "src/index.ts"), - dev: { - remote, - server: { port: 0 }, - inspector: { port: 0 }, - }, - }); - onTestFinished(worker?.dispose); + dev: { + remote, + server: { port: 0 }, + inspector: { port: 0 }, + }, + }); + onTestFinished(worker?.dispose); - const inspectorUrl = await worker.inspectorUrl; - assert(inspectorUrl, "missing inspectorUrl"); - const ws = new WebSocket(inspectorUrl.href); + const inspectorUrl = await worker.inspectorUrl; + assert(inspectorUrl, "missing inspectorUrl"); + const ws = new WebSocket(inspectorUrl.href); - const consoleApiMessages: DevToolsEvent<"Runtime.consoleAPICalled">[] = - collectMessagesContaining(ws, "Runtime.consoleAPICalled"); + const consoleApiMessages: DevToolsEvent<"Runtime.consoleAPICalled">[] = + collectMessagesContaining(ws, "Runtime.consoleAPICalled"); - await worker.fetch("http://dummy"); + await worker.fetch("http://dummy"); - await waitFor(() => { - expect(consoleApiMessages).toContainMatchingObject({ - method: "Runtime.consoleAPICalled", - params: expect.objectContaining({ - args: [{ type: "string", value: LARGE_STRING }], - }), + await waitFor(() => { + expect(consoleApiMessages).toContainMatchingObject({ + method: "Runtime.consoleAPICalled", + params: expect.objectContaining({ + args: [{ type: "string", value: LARGE_STRING }], + }), + }); }); - }); - }); + } + ); it("config.dev.{server,inspector} changes, restart the server instance", async () => { await helper.seed({ diff --git a/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts b/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts index 16033619e4b1..8132845a62ae 100644 --- a/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts +++ b/packages/wrangler/src/__tests__/api/startDevWorker/RemoteRuntimeController.test.ts @@ -127,10 +127,9 @@ describe("RemoteRuntimeController", () => { }); vi.mocked(createPreviewSession).mockResolvedValue({ - id: "test-session-id", value: "test-session-value", host: "test.workers.dev", - prewarmUrl: new URL("https://test.workers.dev/prewarm"), + name: "test", }); vi.mocked(createRemoteWorkerInit).mockResolvedValue({ @@ -162,7 +161,6 @@ describe("RemoteRuntimeController", () => { vi.mocked(createWorkerPreview).mockResolvedValue({ value: "test-preview-token", host: "test.workers.dev", - prewarmUrl: new URL("https://test.workers.dev/prewarm"), tailUrl: "wss://test.workers.dev/tail", }); diff --git a/packages/wrangler/src/__tests__/dev/remote-bindings-errors.test.ts b/packages/wrangler/src/__tests__/dev/remote-bindings-errors.test.ts index 22560a52bf7e..2bf547c44725 100644 --- a/packages/wrangler/src/__tests__/dev/remote-bindings-errors.test.ts +++ b/packages/wrangler/src/__tests__/dev/remote-bindings-errors.test.ts @@ -59,10 +59,9 @@ describe("errors during dev with remote bindings", () => { expect, }) => { vi.mocked(createPreviewSession).mockResolvedValue({ - id: "test-session-id", value: "test-session-value", host: "test.workers.dev", - prewarmUrl: new URL("https://test.workers.dev/prewarm"), + name: "test", }); vi.mocked(createWorkerPreview).mockImplementation(async () => { diff --git a/packages/wrangler/src/api/dev.ts b/packages/wrangler/src/api/dev.ts index 040a156b61b1..3ab08d306255 100644 --- a/packages/wrangler/src/api/dev.ts +++ b/packages/wrangler/src/api/dev.ts @@ -222,7 +222,6 @@ export async function unstable_dev( enableContainers: options?.experimental?.enableContainers ?? false, dockerPath, containerEngine: options?.experimental?.containerEngine, - experimentalTailLogs: true, types: false, }; diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index 124f2c15355e..01407556608e 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -263,7 +263,9 @@ async function resolveTriggers( async function resolveConfig( config: Config, - input: StartDevWorkerInput + input: StartDevWorkerInput, + // If the worker name was previously autogenerated, keep the same one + previousName: string | undefined ): Promise<{ config: StartDevWorkerOptions; printCurrentBindings: (registry: WorkerRegistry | null) => void; @@ -308,7 +310,9 @@ async function resolveConfig( const resolved = { name: - getScriptName({ name: input.name, env: input.env }, config) ?? "worker", + getScriptName({ name: input.name, env: input.env }, config) ?? + previousName ?? + crypto.randomUUID(), config: config.configPath, compatibilityDate: getDevCompatibilityDate( entry.projectRoot, @@ -367,9 +371,7 @@ async function resolveConfig( }, assets: assetsOptions, tailConsumers: config.tail_consumers ?? [], - experimental: { - tailLogs: !!input.experimental?.tailLogs, - }, + experimental: {}, streamingTailConsumers: config.streaming_tail_consumers ?? [], } satisfies StartDevWorkerOptions; @@ -591,7 +593,7 @@ export class ConfigController extends Controller { } const { config: resolvedConfig, printCurrentBindings } = - await resolveConfig(fileConfig, input); + await resolveConfig(fileConfig, input, this.latestConfig?.name); if (signal.aborted) { return; diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index 07f92aa22959..713c52b7813b 100644 --- a/packages/wrangler/src/api/startDevWorker/ProxyController.ts +++ b/packages/wrangler/src/api/startDevWorker/ProxyController.ts @@ -403,21 +403,19 @@ export class ProxyController extends Controller { } get inspectorEnabled() { + // In remote mode, there's no inspector URL available — logs use tail_url instead + if (this.latestConfig?.dev.remote) { + return false; + } + // If we're in a JavaScript Debug terminal, Miniflare will send the inspector ports directly to VSCode for registration // As such, we don't need our inspector proxy and in fact including it causes issue with multiple clients connected to the // inspector endpoint. const inVscodeJsDebugTerminal = !!process.env.VSCODE_INSPECTOR_OPTIONS; - const shouldEnableInspector = - this.latestConfig?.dev.inspector !== false && !inVscodeJsDebugTerminal; - - if (this.latestConfig?.dev.remote) { - // In `wrangler dev --remote`, only enable the inspector if the `--x-tail-logs` flag is disabled - return ( - shouldEnableInspector && !this.latestConfig?.experimental?.tailLogs - ); - } - return shouldEnableInspector; + return ( + this.latestConfig?.dev.inspector !== false && !inVscodeJsDebugTerminal + ); } // ****************** diff --git a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts index dca0b944b5cd..11ce2d03df0d 100644 --- a/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts @@ -53,7 +53,7 @@ export class RemoteRuntimeController extends RuntimeController { async #previewSession( props: Parameters[0] & { - tail_logs: boolean; + name: string; } ): Promise { try { @@ -65,7 +65,7 @@ export class RemoteRuntimeController extends RuntimeController { workerAccount, workerContext, this.#abortController.signal, - props.tail_logs + props.name ); } catch (err: unknown) { if (err instanceof Error && err.name == "AbortError") { @@ -85,12 +85,10 @@ export class RemoteRuntimeController extends RuntimeController { } async #previewToken( - props: Omit & - Partial> & + props: CreateRemoteWorkerInitProps & Parameters[0] & { bundleId: number; minimal_mode?: boolean; - tail_logs: boolean; } ): Promise { if (!this.#session) { @@ -114,6 +112,9 @@ export class RemoteRuntimeController extends RuntimeController { if (props.bundleId !== this.#currentBundleId) { return; } + // Suppress errors from terminating a WebSocket that hasn't connected yet + this.#activeTail?.removeAllListeners("error"); + this.#activeTail?.on("error", () => {}); this.#activeTail?.terminate(); const { workerAccount, workerContext } = await getWorkerAccountAndContext( { @@ -128,12 +129,6 @@ export class RemoteRuntimeController extends RuntimeController { } ); - const scriptId = - props.name || - (workerContext.zone - ? this.#session.id - : this.#session.host.split(".")[0]); - // If we received a new `bundleComplete` event before we were able to // dispatch a `reloadComplete` for this bundle, ignore this bundle. if (props.bundleId !== this.#currentBundleId) { @@ -144,7 +139,7 @@ export class RemoteRuntimeController extends RuntimeController { bundle: props.bundle, modules: props.modules, accountId: props.accountId, - name: scriptId, + name: props.name, useServiceEnvironments: props.useServiceEnvironments, env: props.env, isWorkersSite: props.isWorkersSite, @@ -171,7 +166,7 @@ export class RemoteRuntimeController extends RuntimeController { props.minimal_mode ); - if (props.tail_logs && workerPreviewToken.tailUrl) { + if (workerPreviewToken.tailUrl) { this.#activeTail = new WebSocket( workerPreviewToken.tailUrl, TRACE_VERSION, @@ -226,7 +221,7 @@ export class RemoteRuntimeController extends RuntimeController { routes, sendMetrics: config.sendMetrics, configPath: config.config, - tail_logs: !!config.experimental?.tailLogs, + name: config.name, }); } @@ -291,7 +286,6 @@ export class RemoteRuntimeController extends RuntimeController { configPath: config.config, bundleId, minimal_mode: config.dev.remote === "minimal", - tail_logs: !!config.experimental?.tailLogs, }); // If we received a new `bundleComplete` event before we were able to // dispatch a `reloadComplete` for this bundle, ignore this bundle. @@ -312,16 +306,6 @@ export class RemoteRuntimeController extends RuntimeController { hostname: token.host, port: "443", }, - ...(!config.experimental?.tailLogs && token.inspectorUrl - ? { - userWorkerInspectorUrl: { - protocol: token.inspectorUrl.protocol, - hostname: token.inspectorUrl.hostname, - port: token.inspectorUrl.port.toString(), - pathname: token.inspectorUrl.pathname, - }, - } - : {}), headers: { "cf-workers-preview-token": token.value, ...(accessToken ? { Cookie: `CF_Authorization=${accessToken}` } : {}), @@ -354,6 +338,12 @@ export class RemoteRuntimeController extends RuntimeController { logger.log(chalk.dim("⎔ Detected changes, restarted server.")); } + // Recreate session if the worker name changed, since the session + // host bakes in the name from creation time. + if (this.#session && config.name !== this.#session.name) { + this.#session = undefined; + } + this.#session ??= await this.#getPreviewSession(config, auth, routes); await this.#updatePreviewToken(config, bundle, auth, routes, id); } catch (error) { @@ -460,6 +450,9 @@ export class RemoteRuntimeController extends RuntimeController { logger.debug("RemoteRuntimeController teardown beginning..."); this.#session = undefined; this.#abortController.abort(); + // Suppress errors from terminating a WebSocket that hasn't connected yet + this.#activeTail?.removeAllListeners("error"); + this.#activeTail?.on("error", () => {}); this.#activeTail?.terminate(); logger.debug("RemoteRuntimeController teardown complete"); } diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index e3e698d3542a..c8617ab61398 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -197,9 +197,7 @@ export interface StartDevWorkerInput { unsafe?: Omit; assets?: string; - experimental?: { - tailLogs: boolean; - }; + experimental?: Record; } export type StartDevWorkerOptions = Omit< diff --git a/packages/wrangler/src/dev.ts b/packages/wrangler/src/dev.ts index 21b1fac056a4..2ded604af94f 100644 --- a/packages/wrangler/src/dev.ts +++ b/packages/wrangler/src/dev.ts @@ -255,14 +255,6 @@ export const dev = createCommand({ "Show interactive dev session (defaults to true if the terminal supports interactivity)", type: "boolean", }, - "experimental-tail-logs": { - type: "boolean", - alias: ["x-tail-logs"], - describe: - "Experimental: Get runtime logs for the remote worker via Workers Tails rather than the Devtools inspector", - default: true, - hidden: true, - }, types: { describe: "Generate types from your Worker configuration", type: "boolean", diff --git a/packages/wrangler/src/dev/create-worker-preview.ts b/packages/wrangler/src/dev/create-worker-preview.ts index 8b87cc94f0a7..383efed694ba 100644 --- a/packages/wrangler/src/dev/create-worker-preview.ts +++ b/packages/wrangler/src/dev/create-worker-preview.ts @@ -5,8 +5,8 @@ import { fetch } from "undici"; import { fetchResult } from "../cfetch"; import { createWorkerUploadForm } from "../deployment-bundle/create-worker-upload-form"; import { logger } from "../logger"; +import { getWorkersDevSubdomain } from "../routes"; import { getAccessToken } from "../user/access"; -import { isAbortError } from "../utils/isAbortError"; import type { ApiCredentials } from "../user"; import type { CfWorkerInitWithName } from "./remote"; import type { @@ -36,10 +36,6 @@ export interface CfAccount { * A Preview Session on the edge */ export interface CfPreviewSession { - /** - * A randomly generated id for this session - */ - id: string; /** * A value to use when creating a worker preview under a session */ @@ -49,27 +45,10 @@ export interface CfPreviewSession { */ host: string; /** - * A websocket url to a DevTools inspector. - * - * Workers does not have a fully-featured implementation - * of the Chrome DevTools protocol, but supports the following: - * * `console.log()` output. - * * `Error` stack traces. - * * `fetch()` events. - * - * There is no support for breakpoints, but we want to implement - * this eventually. - * - * @link https://chromedevtools.github.io/devtools-protocol/ - */ - inspectorUrl?: URL; - /** - * A url to prewarm the preview session. - * - * @example - * fetch(prewarmUrl, { method: 'POST' }) + * The worker name used when the session was created. + * Used to detect when the session needs to be recreated. */ - prewarmUrl: URL; + name: string | undefined; } /** @@ -108,32 +87,6 @@ export interface CfPreviewToken { * The host where the preview is available. */ host: string; - /** - * A websocket url to a DevTools inspector. - * - * Workers does not have a fully-featured implementation - * of the Chrome DevTools protocol, but supports the following: - * * `console.log()` output. - * * `Error` stack traces. - * * `fetch()` events. - * - * There is no support for breakpoints, but we want to implement - * this eventually. - * - * @link https://chromedevtools.github.io/devtools-protocol/ - */ - inspectorUrl?: URL; - /** - * A url to prewarm the preview session. - * - * @example - * fetch(prewarmUrl, { method: 'POST', - * headers: { - * "cf-workers-preview-token": (preview)token.value, - * } - * }) - */ - prewarmUrl: URL; /** * A URL that when fetched starts a tail. Essentially, `wrangler tail` for realish previews. * @@ -156,6 +109,66 @@ function switchHost( url.hostname = zonePreview ? host ?? url.hostname : url.hostname; return url; } + +/** + * Try and get a re-encoded token from the edge. Returns null if the exchange + * fails for any reason (expected with particular zone settings). + * Rethrows AbortError so callers can handle cancellation. + */ +async function tryExpandToken( + exchangeUrl: string, + ctx: CfWorkerContext, + abortSignal: AbortSignal +): Promise { + try { + const switchedExchangeUrl = switchHost(exchangeUrl, ctx.host, !!ctx.zone); + + const headers: HeadersInit = {}; + const accessToken = await getAccessToken(switchedExchangeUrl.hostname); + + if (accessToken) { + headers.cookie = `CF_Authorization=${accessToken}`; + } + + logger.debugWithSanitization( + "-- START EXCHANGE API REQUEST:", + ` GET ${switchedExchangeUrl.href}` + ); + + logger.debug("-- END EXCHANGE API REQUEST"); + const exchangeResponse = await fetch(switchedExchangeUrl, { + signal: abortSignal, + headers, + }); + const bodyText = await exchangeResponse.text(); + logger.debug( + "-- START EXCHANGE API RESPONSE:", + exchangeResponse.statusText, + exchangeResponse.status + ); + logger.debug("HEADERS:", JSON.stringify(exchangeResponse.headers, null, 2)); + logger.debugWithSanitization("RESPONSE:", bodyText); + + logger.debug("-- END EXCHANGE API RESPONSE"); + + if (!exchangeResponse.ok) { + return null; + } + + const body = parseJSON(bodyText) as { + token?: string; + }; + if (typeof body?.token !== "string") { + return null; + } + return body.token; + } catch (e) { + if (e instanceof Error && e.name === "AbortError") { + throw e; + } + return null; + } +} /** * Generates a preview session token. */ @@ -164,68 +177,37 @@ export async function createPreviewSession( account: CfAccount, ctx: CfWorkerContext, abortSignal: AbortSignal, - tailLogs: boolean + name: string | undefined ): Promise { const { accountId, apiToken } = account; const initUrl = ctx.zone ? `/zones/${ctx.zone}/workers/edge-preview` : `/accounts/${accountId}/workers/subdomain/edge-preview`; - const { exchange_url } = await fetchResult<{ exchange_url: string }>( - complianceConfig, - initUrl, - undefined, - undefined, - abortSignal, - apiToken - ); - - const switchedExchangeUrl = switchHost(exchange_url, ctx.host, !!ctx.zone); + const { token, exchange_url } = await fetchResult<{ + token: string; + exchange_url?: string; + }>(complianceConfig, initUrl, undefined, undefined, abortSignal, apiToken); - const headers: HeadersInit = {}; - const accessToken = await getAccessToken(switchedExchangeUrl.hostname); - - if (accessToken) { - headers.cookie = `CF_Authorization=${accessToken}`; - } + const previewSessionToken = exchange_url + ? (await tryExpandToken(exchange_url, ctx, abortSignal)) ?? token + : token; - logger.debugWithSanitization( - "-- START EXCHANGE API REQUEST:", - ` GET ${switchedExchangeUrl.href}` - ); - - logger.debug("-- END EXCHANGE API REQUEST"); - const exchangeResponse = await fetch(switchedExchangeUrl, { - signal: abortSignal, - headers, - }); - const bodyText = await exchangeResponse.text(); - logger.debug( - "-- START EXCHANGE API RESPONSE:", - exchangeResponse.statusText, - exchangeResponse.status - ); - logger.debug("HEADERS:", JSON.stringify(exchangeResponse.headers, null, 2)); - logger.debugWithSanitization("RESPONSE:", bodyText); - - logger.debug("-- END EXCHANGE API RESPONSE"); try { - const { inspector_websocket, prewarm, token } = parseJSON(bodyText) as { - inspector_websocket: string; - token: string; - prewarm: string; - }; - let inspectorUrl: URL | undefined; - if (!tailLogs) { - inspectorUrl = switchHost(inspector_websocket, ctx.host, !!ctx.zone); - inspectorUrl.searchParams.append("cf_workers_preview_token", token); + let host = ctx.host; + if (!host) { + const subdomain = await getWorkersDevSubdomain( + complianceConfig, + account.accountId, + undefined, + apiToken + ); + host = `${name ?? crypto.randomUUID()}.${subdomain}`; } return { - id: crypto.randomUUID(), - value: token, - host: ctx.host ?? inspectorUrl?.host ?? switchedExchangeUrl.host, - prewarmUrl: switchHost(prewarm, ctx.host, !!ctx.zone), - inspectorUrl, + value: previewSessionToken, + host: host, + name, }; } catch (e) { if (!(e instanceof ParseError)) { @@ -254,7 +236,7 @@ async function createPreviewToken( abortSignal: AbortSignal, minimal_mode?: boolean ): Promise { - const { value, host, inspectorUrl, prewarmUrl } = session; + const { value, host } = session; const { accountId } = account; const url = ctx.env && ctx.useServiceEnvironments @@ -303,22 +285,7 @@ async function createPreviewToken( return { value: preview_token, - host: - ctx.host ?? - (worker.name - ? `${ - worker.name - // TODO: this should also probably have the env prefix - // but it doesn't appear to work yet, instead giving us the - // "There is nothing here yet" screen - // ctx.env && ctx.useServiceEnvironments - // ? `${ctx.env}.${worker.name}` - // : worker.name - }.${host.split(".").slice(1).join(".")}` - : host), - - inspectorUrl, - prewarmUrl, + host, tailUrl: tail_url, }; } @@ -347,30 +314,6 @@ export async function createWorkerPreview( abortSignal, minimal_mode ); - const accessToken = await getAccessToken(token.prewarmUrl.hostname); - - const headers: HeadersInit = { "cf-workers-preview-token": token.value }; - if (accessToken) { - headers.cookie = `CF_Authorization=${accessToken}`; - } - - // fire and forget the prewarm call - fetch(token.prewarmUrl.href, { - method: "POST", - signal: abortSignal, - headers, - }).then( - (response) => { - if (!response.ok) { - logger.warn("worker failed to prewarm: ", response.statusText); - } - }, - (err) => { - if (isAbortError(err)) { - logger.warn("worker failed to prewarm: ", err); - } - } - ); return token; } diff --git a/packages/wrangler/src/dev/hotkeys.ts b/packages/wrangler/src/dev/hotkeys.ts index d03b2e1e5edb..4caae69d3e97 100644 --- a/packages/wrangler/src/dev/hotkeys.ts +++ b/packages/wrangler/src/dev/hotkeys.ts @@ -13,7 +13,6 @@ export default function registerDevHotKeys( devEnvs: DevEnv[], args: { forceLocal?: boolean; - experimentalTailLogs: boolean; remote: boolean; }, render = true @@ -33,9 +32,7 @@ export default function registerDevHotKeys( keys: ["d"], label: "open devtools", // Don't display this hotkey if we're in a VSCode debug session - disabled: - !!process.env.VSCODE_INSPECTOR_OPTIONS || - (args.remote && args.experimentalTailLogs), + disabled: !!process.env.VSCODE_INSPECTOR_OPTIONS || args.remote, handler: async () => { const { inspectorUrl } = await primaryDevEnv.proxy.ready.promise; diff --git a/packages/wrangler/src/dev/start-dev.ts b/packages/wrangler/src/dev/start-dev.ts index 50e0b05f78f6..cf061e0661f1 100644 --- a/packages/wrangler/src/dev/start-dev.ts +++ b/packages/wrangler/src/dev/start-dev.ts @@ -286,9 +286,6 @@ async function setupDevEnv( useServiceEnvironments: !(args.legacyEnv ?? true), }, assets: args.assets, - experimental: { - tailLogs: !!args.experimentalTailLogs, - }, } satisfies StartDevWorkerInput, true ); diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index 7218c2ffa7fa..a015af59f222 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -1023,7 +1023,6 @@ export const pagesDevCommand = createCommand({ siteInclude: undefined, siteExclude: undefined, enableContainers: false, - experimentalTailLogs: true, types: false, }) ); diff --git a/packages/wrangler/src/routes.ts b/packages/wrangler/src/routes.ts index b9d27d63ae6c..a09ff0189c2b 100644 --- a/packages/wrangler/src/routes.ts +++ b/packages/wrangler/src/routes.ts @@ -7,6 +7,7 @@ import chalk from "chalk"; import { fetchResult } from "./cfetch"; import { confirm, prompt } from "./dialogs"; import { logger } from "./logger"; +import type { ApiCredentials } from "./user/user"; import type { ComplianceConfig } from "@cloudflare/workers-utils"; /** @@ -15,13 +16,18 @@ import type { ComplianceConfig } from "@cloudflare/workers-utils"; export async function getWorkersDevSubdomain( complianceConfig: ComplianceConfig, accountId: string, - configPath: string | undefined + configPath: string | undefined, + apiToken?: ApiCredentials ): Promise { try { // note: API docs say that this field is "name", but they're lying. const { subdomain } = await fetchResult<{ subdomain: string }>( complianceConfig, - `/accounts/${accountId}/workers/subdomain` + `/accounts/${accountId}/workers/subdomain`, + undefined, + undefined, + undefined, + apiToken ); return `${subdomain}${getComplianceRegionSubdomain(complianceConfig)}.workers.dev`; } catch (e) {