Skip to content

[remote-bindings] Extract remote bindings + dev proxy into standalone packages#14618

Open
penalosa wants to merge 10 commits into
mainfrom
penalosa/extract-dev-env
Open

[remote-bindings] Extract remote bindings + dev proxy into standalone packages#14618
penalosa wants to merge 10 commits into
mainfrom
penalosa/extract-dev-env

Conversation

@penalosa

@penalosa penalosa commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Extract Wrangler's remote-bindings machinery into standalone packages so consumers (starting with the Vite plugin) can establish remote-binding proxy sessions without depending on wrangler, while keeping a single source of truth for the proxy logic.

What changed

  • New @cloudflare/dev-proxy — single home for the ProxyWorker. Wrangler's ProxyController now consumes it (via a miniflare-free /protocol entry point so the workerd InspectorProxyWorker keeps building). Also shares the remote-mode ProxyData construction used by wrangler dev --remote.
  • New @cloudflare/remote-bindings — edge-preview primitives (create-worker-preview, pick-remote-bindings, env-driven auth) plus start-session.ts, which drives the shared ProxyWorker inside Miniflare exactly as wrangler dev --remote does (proactive + reactive preview-token refresh).
  • Wrangler now delegatesstart-remote-proxy-session.ts is a thin wrapper over the package (wiring wrangler's logger + requireAuth); create-worker-preview is de-duplicated into remote-bindings with a small dev/preview.ts shim that injects wrangler's logger + interactivity predicate (behaviour preserved).
  • Vite plugin — drives remote bindings via a lazy import("@cloudflare/remote-bindings") (keeps the Node-only package out of Vite's pure-ESM config-load graph) instead of wrangler.
  • Small supporting additions to workers-utils / workers-auth / deploy-helpers (env-var unions, shared types).

Single source of truth: wrangler loses this logic (net reduction) and the new packages gain it — no reimplementation of the proxy path.


  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:
  • Public documentation
    • Cloudflare docs PR(s):
    • Documentation not necessary because: internal refactor/extraction — no change to Wrangler's public API or CLI behaviour.

A picture of a cute animal (not mandatory, but encouraged)

penalosa added 6 commits July 9, 2026 00:55
Create a new @cloudflare/dev-proxy package as the single source of truth
for the ProxyWorker and its type/util cluster (createDeferred, urlFromParts,
ProxyData, proxy-worker protocol types, serialiseError) plus a Miniflare
harness (createProxyWorkerOptions/sendProxyWorkerMessage).

Repoint wrangler onto it: startDevWorker/utils.ts and events.ts become
re-export shims, and ProxyController builds the ProxyWorker via the shared
harness (inline contents) instead of the worker: embed. Behaviour is
unchanged - all startDevWorker tests pass.
…dings auth

Support @cloudflare/remote-bindings running outside wrangler:

- workers-utils: add CLOUDFLARE_CONFIG_DIR / CLOUDFLARE_AUTH_CONFIG_FILE /
  CLOUDFLARE_OAUTH_CLIENT_ID / CLOUDFLARE_ALLOW_GLOBAL_API_KEY /
  CLOUDFLARE_LOGIN_COMMAND to the env-var name union; export
  getGlobalConfigDirFromEnv and honour CLOUDFLARE_CONFIG_DIR in
  getGlobalConfigPath; add the shared CfWorkerInitWithName type used by both
  wrangler's dev path and remote bindings when building preview uploads.
- workers-auth: export getClientIdFromEnv (CLOUDFLARE_OAUTH_CLIENT_ID reader)
  so a delegated tool can refresh a stored OAuth token.
Introduce @cloudflare/remote-bindings, a standalone package (usable without
wrangler) for proxying a Worker's remote bindings to the Cloudflare edge.

This commit lands the proven, edge-facing pieces:
- pick-remote-bindings: select bindings that must resolve remotely
- create-worker-preview: edge-preview session + token upload via the shared
  workers-utils API client (single source of truth, no duplicated auth/URL
  handling)
- auth: env-var/OAuth credential resolver for delegated (non-wrangler) use
- ProxyServerWorker: the minimal-mode edge worker that dispatches proxied
  requests to the real bindings (bundled inline via a tsdown virtual module)
- types/logger + barrel exports

The Miniflare-backed local driver (start-session) follows in a subsequent
commit.
Extract the remote-mode ProxyData shape (HTTPS to the preview host on 443,
injecting cf-workers-preview-token + Access headers) from wrangler's
RemoteRuntimeController into createRemoteModeProxyData in @cloudflare/dev-proxy,
so wrangler dev --remote and @cloudflare/remote-bindings drive the shared
ProxyWorker with an identical, single-sourced configuration.

RemoteRuntimeController now calls the shared helper; behaviour is unchanged
(RemoteRuntimeController tests pass).
Add the local driver (startRemoteProxySession / maybeStartOrUpdateRemoteProxySession):
instead of a bespoke HTTP/WS proxy, run the SAME ProxyWorker as wrangler dev
(@cloudflare/dev-proxy) inside Miniflare, configured for remote mode via the
shared createRemoteModeProxyData. Token injection, expiry detection and
HTTP/WS forwarding are therefore wrangler's proven logic, not a reimplementation.

The 1-hour preview token is refreshed both proactively (shared
PREVIEW_TOKEN_REFRESH_INTERVAL, now sourced from dev-proxy and re-exported by
wrangler's utils) and reactively on the ProxyWorker's previewTokenExpired
message, mirroring RemoteRuntimeController.
The ProxyWorker harness depends on miniflare, but wrangler's InspectorProxyWorker
(a workerd worker) imports wrangler's utils/events re-export shims. Importing the
dev-proxy barrel from those shims dragged miniflare into the worker bundle,
breaking it at runtime (No such module "fs/promises").

Split the pure protocol layer (types, control-message shapes, createDeferred,
urlFromParts, createRemoteModeProxyData, serialiseError, refresh interval) into a
@cloudflare/dev-proxy/protocol entry point with no miniflare dependency. The main
entry re-exports it plus the harness. wrangler's worker-facing shims now import
from /protocol so worker bundles stay miniflare-free.
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 504d2d0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@cloudflare/vite-plugin Patch
wrangler Patch
@cloudflare/vitest-pool-workers Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Jul 9, 2026
@penalosa penalosa marked this pull request as ready for review July 9, 2026 00:02
@penalosa penalosa requested a review from workers-devprod as a code owner July 9, 2026 00:02
@workers-devprod workers-devprod requested review from a team and removed request for a team July 9, 2026 00:02
@workers-devprod

workers-devprod commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/extract-remote-bindings.md: [@cloudflare/wrangler]
  • .oxlintrc.jsonc: [@cloudflare/wrangler]
  • packages/dev-proxy/package.json: [@cloudflare/wrangler]
  • packages/dev-proxy/scripts/deps.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/src/deferred.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/src/harness.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/src/index.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/src/protocol.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/src/proxy-data.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/src/url.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/src/virtual.d.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/templates/ProxyWorker.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/tsconfig.json: [@cloudflare/wrangler]
  • packages/dev-proxy/tsdown.config.ts: [@cloudflare/wrangler]
  • packages/dev-proxy/turbo.json: [@cloudflare/wrangler]
  • packages/remote-bindings/package.json: [@cloudflare/wrangler]
  • packages/remote-bindings/scripts/deps.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/auth.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/create-worker-preview.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/index.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/logger.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/maybe-start-session.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/pick-remote-bindings.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/start-session.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/types.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/virtual.d.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/templates/ProxyServerWorker.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/test/stubs/proxy-server-worker.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/test/auth.test.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/test/pick-remote-bindings.test.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/tsconfig.json: [@cloudflare/wrangler]
  • packages/remote-bindings/tsdown.config.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/turbo.json: [@cloudflare/wrangler]
  • packages/remote-bindings/vitest.config.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/package.json: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/workers-auth/src/env-vars.ts: [@cloudflare/wrangler]
  • packages/workers-auth/src/index.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/environment-variables/factory.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/global-wrangler-config-path.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/index.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/types.ts: [@cloudflare/wrangler]
  • packages/wrangler/package.json: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/api/startDevWorker/RemoteRuntimeController.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/dev/remote-bindings-errors.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/remoteBindings/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/remoteBindings/start-remote-proxy-session.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/ProxyController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/events.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/utils.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/test-harness.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/preview.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/remote.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/start-dev.ts: [@cloudflare/wrangler]
  • packages/wrangler/templates/remoteBindings/wrangler.jsonc: [@cloudflare/wrangler]
  • pnpm-lock.yaml: [@cloudflare/wrangler]
  • tools/deployments/tests/validate-changesets.test.ts: [@cloudflare/wrangler]

@workers-devprod workers-devprod requested a review from ascorbic July 9, 2026 00:02
@penalosa penalosa removed the request for review from ascorbic July 9, 2026 00:02
@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

I'm Bonk, and I've done a quick review of your PR.

Extracts Wrangler's remote-bindings + dev-proxy machinery into standalone @cloudflare/dev-proxy and @cloudflare/remote-bindings packages, with Wrangler and the Vite plugin delegating to them. The extraction is well-factored (single source of truth for ProxyWorker, the /protocol split that keeps the workerd InspectorProxyWorker buildable, and the CJS-shim banners), and the wrapper shims preserve behaviour in the common paths.

One behaviour regression stands out, plus a minor teardown race.

Issues (highest severity first)

1. [medium] Empty-bindings regression in the public maybeStartOrUpdateRemoteProxySession API

The new package impl now throws "Cannot start remote proxy session with no bindings" when bindings is empty:

packages/remote-bindings/src/start-session.ts:150

if (Object.keys(bindings).length === 0) {
    throw new Error("Cannot start remote proxy session with no bindings");
}

On main, startRemoteProxySession({}) proceeded without throwing (it built rawBindings from {} and started the worker). In wrangler's own public API, packages/wrangler/src/api/remoteBindings/index.ts calls it unconditionally in the !authSameAsBefore branch:

packages/wrangler/src/api/remoteBindings/index.ts:105-123

if (!authSameAsBefore) {
    ...
    remoteProxySession = await startRemoteProxySession(remoteBindings, { ... }); // remoteBindings may be {}
} else {
    ...
    if (!remoteProxySession) {
        if (Object.keys(remoteBindings).length > 0) {   // <-- guarded here
            remoteProxySession = await startRemoteProxySession(remoteBindings, { ... });
        }
    }
    ...
}

The else branch guards with Object.keys(remoteBindings).length > 0, but the !authSameAsBefore branch does not. So a worker with no remote bindings but a changed/supplied auth hook (reachable via the exported maybeStartOrUpdateRemoteProxySession API and the api/integrations/platform caller) now throws where it previously worked. Note the remote-bindings package's own maybe-start-session.ts handles this correctly (returns null early when there are no remote bindings) — mirroring that guard in the !authSameAsBefore branch restores prior behaviour:

	if (!authSameAsBefore) {
		// The auth values have changed so we do need to restart a new remote proxy session

		if (preExistingRemoteProxySessionData?.session) {
			await preExistingRemoteProxySessionData.session.dispose();
		}

		if (Object.keys(remoteBindings).length > 0) {
			remoteProxySession = await startRemoteProxySession(remoteBindings, {
				workerName: workerConfigObject.name,
				complianceRegion: workerConfigObject.complianceRegion,
				auth: getAuthHook(
					auth,
					workerConfigObject.account_id
						? {
								account_id: workerConfigObject.account_id,
							}
						: config
				),
			});
		} else {
			remoteProxySession = undefined;
		}
	} else {

2. [low] Teardown race in start-session.ts

packages/remote-bindings/src/start-session.ts:322-326dispose() sets disposed = true and disposes Miniflare, but an in-flight refreshToken() that already passed its disposed guard (or a concurrent updateBindings(), which doesn't check disposed at all) can still call play()sendProxyWorkerMessage() against a disposed instance. Consider awaiting any in-flight refresh before disposing:

	const dispose = async (): Promise<void> => {
		disposed = true;
		clearTimeout(refreshTimer);
		await refreshing?.catch(() => {});
		await mf.dispose();
	};

@ask-bonk

ask-bonk Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Posted the review to PR #14618. Summary of what I found:

  1. [medium] Empty-bindings regression in wrangler's public maybeStartOrUpdateRemoteProxySession API. The extracted startRemoteProxySession now throws on empty bindings (previously it accepted them), and the !authSameAsBefore branch in packages/wrangler/src/api/remoteBindings/index.ts:105-123 calls it unconditionally — unlike the parallel else branch, which guards with a length check. Reachable via the exported API and the platform-integration caller. Suggested restoring the guard.

  2. [low] Teardown race in start-session.ts's dispose() — an in-flight proactive refresh or concurrent updateBindings() can send a control message to an already-disposed Miniflare. Suggested awaiting the in-flight refresh before disposing.

The extraction itself (dev-proxy /protocol split, shared ProxyWorker/createRemoteModeProxyData, CJS-shim banners, lazy Vite import) is otherwise sound and preserves behaviour on the common paths.

github run

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

Open in Devin Review

Comment on lines +304 to +305
await play(token);
scheduleRefresh();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Local proxy server leaks when initial configuration fails

The local proxy server is started and left running if the initial configuration step fails (play(token) at packages/remote-bindings/src/start-session.ts:304), because no cleanup disposes the server before the error propagates.

Impact: A Miniflare process remains running on a random port with no handle to shut it down, leaking system resources on every failed session start.

Miniflare instance created but never disposed on play() failure

The Miniflare instance mf is created at packages/remote-bindings/src/start-session.ts:219 and its ready promise is awaited at line 299. If play(token) at line 304 throws (e.g. a network error sending the proxy worker message via sendProxyWorkerMessage), the error propagates out of startRemoteProxySession without ever calling mf.dispose(). The dispose closure is only returned as part of the session object at line 332, which is never reached.

The caller (packages/remote-bindings/src/start-session.ts:176-179) wraps the error but has no reference to mf to clean up.

Prompt for agents
In packages/remote-bindings/src/start-session.ts, the code after `const url = await mf.ready` (around line 299) calls `play(token)` and `scheduleRefresh()` without a try/catch. If either throws, the Miniflare instance `mf` is leaked because `dispose()` is only available on the returned session object, which is never constructed.

Wrap lines 304-305 in a try/catch that calls `await mf.dispose()` before re-throwing. Something like:

  try {
    await play(token);
    scheduleRefresh();
  } catch (error) {
    disposed = true;
    clearTimeout(refreshTimer);
    await mf.dispose();
    throw error;
  }

This ensures the Miniflare server is cleaned up if the initial play fails.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +311 to +314
currentBindings = newBindings;
token = await uploadToken(newBindings);
await play(token);
scheduleRefresh();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Binding state corrupted on failed update, causing repeated refresh failures

The current binding state is overwritten with new values (currentBindings = newBindings at packages/remote-bindings/src/start-session.ts:311) before the upload succeeds, so a failed update poisons the periodic token refresh with the bad bindings.

Impact: After a single failed binding update, the automatic token refresh (every 50 minutes) silently retries with the unapplied bindings instead of the last working set, potentially causing repeated failures.

Premature mutation of currentBindings before async uploadToken completes

updateBindings at packages/remote-bindings/src/start-session.ts:307-320 sets currentBindings = newBindings on line 311, then calls await uploadToken(newBindings) on line 312. If uploadToken throws, the error is re-thrown at line 316, but currentBindings has already been mutated to the failed value.

The refreshToken function at line 260-278 calls uploadToken(currentBindings) on line 269, so it will now use the bindings that failed to upload rather than the last-known-good set.

The fix is to defer the assignment until after uploadToken and play succeed:

const newToken = await uploadToken(newBindings);
await play(newToken);
currentBindings = newBindings;
token = newToken;
scheduleRefresh();
Suggested change
currentBindings = newBindings;
token = await uploadToken(newBindings);
await play(token);
scheduleRefresh();
const newToken = await uploadToken(newBindings);
await play(newToken);
currentBindings = newBindings;
token = newToken;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

try {
if (isJSRPCBinding(request)) {
return await newWorkersRpcResponse(
return newWorkersRpcResponse(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Removing await before return inside try/catch silently bypasses error handling

The ProxyServerWorker template changes return await newWorkersRpcResponse(...) to return newWorkersRpcResponse(...) and return await fetcher.fetch(...) to return fetcher.fetch(...) at packages/remote-bindings/templates/ProxyServerWorker.ts:111 and packages/remote-bindings/templates/ProxyServerWorker.ts:128. Both are inside an async function's try/catch block. This is a well-known JavaScript footgun: when you return await inside a try/catch, the promise settles within the try block so rejections are caught. When you just return a promise, it's returned directly and rejections bypass the catch block entirely. The catch block at line 136-141 handles BindingNotFoundError (returning 400) and generic errors (returning 500 with the error message). After this change, if newWorkersRpcResponse or fetcher.fetch returns a rejected promise, the formatted error response is lost and the rejection propagates unhandled to the workerd runtime instead.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +260 to +278
function refreshToken(): Promise<void> {
if (disposed) {
return Promise.resolve();
}
if (refreshing) {
return refreshing;
}
refreshing = (async () => {
try {
token = await uploadToken(currentBindings);
await play(token);
scheduleRefresh();
} catch (error) {
logger.error("Failed to refresh remote bindings preview token", error);
} finally {
refreshing = undefined;
}
})();
return refreshing;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Preview session is never recreated during token refresh, unlike wrangler's RemoteRuntimeController

In packages/remote-bindings/src/start-session.ts:260-278, the refreshToken function only re-uploads the worker and re-plays the token, but it reuses the original session object (captured in the closure at line 165). In contrast, wrangler's RemoteRuntimeController.#refreshPreviewToken (packages/wrangler/src/api/startDevWorker/RemoteRuntimeController.ts:427-431) recreates the preview session before refreshing the token. If the preview session itself becomes invalid (not just the token), the remote-bindings refresh path would fail repeatedly without recovery. This may be acceptable for the remote-bindings use case (shorter-lived sessions), but it's a behavioral difference from wrangler's proven pattern worth noting.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@workers-devprod workers-devprod requested review from a team and emily-shen and removed request for a team July 9, 2026 00:07
@penalosa penalosa force-pushed the penalosa/extract-dev-env branch from f1f4e24 to 50a3919 Compare July 9, 2026 00:10
penalosa added 3 commits July 9, 2026 01:12
Replace wrangler's startWorker-based remote proxy session (which spun up a full
DevEnv with an embedded ProxyServerWorker template) with a thin wrapper over
@cloudflare/remote-bindings' startRemoteProxySession. Wrangler now wires in its
logger + auth system and preserves its error surfacing (UserErrors re-thrown
directly), while the actual proxying uses the shared ProxyWorker via the package.

Remove the now-unused worker:remoteBindings/ProxyServerWorker template and its
wrangler.jsonc. Update remote-bindings-errors.test.ts to exercise the real
package path (the previous test mocked wrangler's create-worker-preview and
asserted DevEnv-internal ErrorEvent shapes that no longer apply).

All wrangler remote-bindings unit tests pass (21).
The ProxyWorker (dev-proxy) and ProxyServerWorker (remote-bindings) templates
are embedded worker scripts bundled at build time; like wrangler/templates they
use workers-types globals and edge-only patterns incompatible with the
Node-focused lint rules. Add both dirs to ignorePatterns, matching the existing
packages/wrangler/templates exclusion.
Repoint remote proxy sessions from wrangler's re-exported helper to the
standalone @cloudflare/remote-bindings package. Import it lazily (dynamic
import at the call site) so the Node-only package stays out of the plugin's
config-load module graph — Vite evaluates the config in a pure-ESM context.

Import the RemoteProxySession type from @cloudflare/remote-bindings and switch
the session config field from account_id to the package's accountId.
@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/autoconfig@14618

create-cloudflare

npm i https://pkg.pr.new/cloudflare/workers-sdk/create-cloudflare@14618

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/deploy-helpers@14618

@cloudflare/dev-proxy

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/dev-proxy@14618

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/kv-asset-handler@14618

miniflare

npm i https://pkg.pr.new/cloudflare/workers-sdk/miniflare@14618

@cloudflare/pages-shared

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/pages-shared@14618

@cloudflare/remote-bindings

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/remote-bindings@14618

@cloudflare/unenv-preset

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/unenv-preset@14618

@cloudflare/vite-plugin

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/vite-plugin@14618

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/vitest-pool-workers@14618

@cloudflare/workers-auth

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/workers-auth@14618

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/workers-editor-shared@14618

@cloudflare/workers-utils

npm i https://pkg.pr.new/cloudflare/workers-sdk/@cloudflare/workers-utils@14618

wrangler

npm i https://pkg.pr.new/cloudflare/workers-sdk/wrangler@14618

commit: 504d2d0

@penalosa penalosa force-pushed the penalosa/extract-dev-env branch from 50a3919 to c75b779 Compare July 9, 2026 00:13

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Open in Devin Review

Comment on lines +260 to +279
function refreshToken(): Promise<void> {
if (disposed) {
return Promise.resolve();
}
if (refreshing) {
return refreshing;
}
refreshing = (async () => {
try {
token = await uploadToken(currentBindings);
await play(token);
scheduleRefresh();
} catch (error) {
logger.error("Failed to refresh remote bindings preview token", error);
} finally {
refreshing = undefined;
}
})();
return refreshing;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Token refresh errors are silently logged, not propagated

In packages/remote-bindings/src/start-session.ts:267-278, when refreshToken() fails (e.g. network error, expired OAuth token that can't be refreshed), the error is logged via logger.error but not propagated. The refresh timer is also not rescheduled on failure (no scheduleRefresh() in the catch block), meaning after a single refresh failure the session will never attempt another proactive refresh — only reactive refreshes triggered by previewTokenExpired messages from the ProxyWorker. This mirrors wrangler's RemoteRuntimeController behavior where refresh failures are also logged-and-swallowed, but the difference is that wrangler's controller re-schedules via #scheduleRefresh in #updatePreviewToken which is always called. Here, a failed refresh means no more proactive refreshes until the next updateBindings call.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread packages/remote-bindings/src/create-worker-preview.ts
@penalosa penalosa force-pushed the penalosa/extract-dev-env branch from c75b779 to 504d2d0 Compare July 9, 2026 00:21
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

✅ All changesets look good

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Open in Devin Review

Comment on lines +307 to +320
const updateBindings = async (
newBindings: Record<string, Binding>
): Promise<void> => {
try {
currentBindings = newBindings;
token = await uploadToken(newBindings);
await play(token);
scheduleRefresh();
} catch (error) {
throw new Error("Failed to update remote proxy bindings", {
cause: error,
});
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 updateBindings and refreshToken are not serialized against each other

In packages/remote-bindings/src/start-session.ts:307-319, updateBindings performs uploadToken + play + scheduleRefresh without any serialization against the concurrent refreshToken function (lines 260-279). While refreshToken serializes against itself via the refreshing promise guard, nothing prevents updateBindings and refreshToken from running simultaneously. If the proactive refresh timer fires while updateBindings is mid-flight, both will call uploadToken concurrently — one with old bindings (from refreshToken reading currentBindings before line 311 executes) and one with new bindings. This could result in a stale token briefly winning the race. The old wrangler implementation used a Mutex (runtimeMessageMutex) and event-based coordination to avoid this. In practice, the window is small and the next scheduled refresh would correct it, but it's a behavioral regression from the old implementation's stricter serialization.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

2 participants