Skip to content

Replace HTTP dev-registry proxy with native workerd debug port RPC#12600

Open
penalosa wants to merge 54 commits intomainfrom
penalosa/native-registry
Open

Replace HTTP dev-registry proxy with native workerd debug port RPC#12600
penalosa wants to merge 54 commits intomainfrom
penalosa/native-registry

Conversation

@penalosa
Copy link
Copy Markdown
Contributor

@penalosa penalosa commented Feb 18, 2026

The dev registry proxy — responsible for forwarding service binding calls between separate wrangler dev / vite dev sessions — has been rewritten to use workerd's native debug port RPC instead of routing everything through a Node.js HTTP proxy thread.

The old approach had a lot of moving parts: a separate Node.js worker thread running an HTTP server, per-service socket allocations in workerd, special-casing for WebSocket upgrades and streamed bodies, and a growing list of edge cases. The new approach runs an ExternalServiceProxy WorkerEntrypoint directly inside workerd and uses the debug port to forward calls natively — less code, fewer layers, and far fewer things that can go wrong.

As a bonus, Durable Object RPC now works across dev sessions — something that was previously blocked behind a "not yet supported" error.


  • 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 dev tooling change, no public API change

Open with Devin

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 18, 2026

🦋 Changeset detected

Latest commit: 811a12b

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

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 Feb 18, 2026
@penalosa penalosa force-pushed the penalosa/native-registry branch 2 times, most recently from b2f8d24 to 5fb825a Compare February 18, 2026 23:05
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Feb 19, 2026

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@12600

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@12600

miniflare

npm i https://pkg.pr.new/miniflare@12600

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@12600

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@12600

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@12600

@cloudflare/vitest-pool-workers

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

@cloudflare/workers-editor-shared

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

wrangler

npm i https://pkg.pr.new/wrangler@12600

commit: 811a12b

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Feb 23, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

name: `${RPC_PROXY_SERVICE_NAME}:${id}`,
worker: {
compatibilityDate: "2024-08-01",
compatibilityFlags: ["service_binding_extra_handlers"],
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is this definitely still needed?

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.

???

penalosa added 17 commits March 2, 2026 13:20
Replace HTTP-based dev registry proxy with native workerd debug port:
- Use WorkerdDebugPort binding for direct Cap'n Proto RPC to remote workers
- Service bindings and DOs now route through debug port instead of HTTP proxy
- Support RPC method calls, fetch, and tail handlers through debug port
- Add HTTP fallback for WebSocket upgrades (debug port RPC doesn't support them)
- Remove old dev-registry.worker.ts HTTP proxy implementation
- Simplify external-service.ts by removing HTTP proxy infrastructure

All 17 dev-registry tests pass including WebSocket upgrades.
The vite dev <-> vite dev tail handler test was missing the waitForTimeout
parameter on one of its waitFor calls, causing it to use the default (shorter)
timeout which could flake in CI.
…port RPC

Remove the HTTP fallback, eager body buffering, _client GC hack, and WebSocket
special-casing from the dev-registry proxy workers. These workarounds existed
because the workerd debug port would close connections prematurely when the
initial subrequest completed. With the refcounted DebugPortConnectionState fix
in workerd (cloudflare/workerd#6100), connections now survive as long as any
response body or WebSocket is in use.

Key changes:
- Replace hasAssets/entryAddress with defaultEntrypointService and
  userWorkerService fields in WorkerDefinition/RegistryEntry
- Route DOs and named entrypoints through userWorkerService (bypassing
  assets/vite proxy layer) instead of hardcoding core:user: prefix
- Validate required registry fields in resolveTarget() to handle stale entries
- Inline and delete getWorkerdServiceName() helper
- Remove dead entryAddress field and fetchViaHttp code path
- Remove DO WebSocket 501 error and DO body buffering workarounds
…uting

Vite workers with assets were incorrectly routing through assets:rpc-proxy
in the dev registry, but in vite dev mode assets are served by the vite
proxy worker, not workerd's asset pipeline. This caused cross-service
fetch to vite workers with assets to fail (timeout).

Add unsafeOverrideDefaultEntrypoint to miniflare's core options schema so
the vite plugin can explicitly specify which workerd service handles the
default entrypoint. This replaces the brittle inference from
unsafeDirectSockets[].serviceName.
…nd tests

After the debug port RPC refactor, unsafeDirectSockets in the user worker
config, vite plugin, and dev-registry tests no longer serve a functional
purpose — nobody reads the socket URLs. The only remaining use is in
wrangler's ProxyController for InspectorProxyWorker (not touched here).
The Reflect.has check already handles keys that exist on the target.
For keys not on the target, the runtime doesn't probe handler properties
through the proxy get trap in a way that requires suppression — confirmed
by tests passing without it.
…ndings instead

Instead of mutating workerOpts with symbol-tagged designators before plugin
processing (requiring every consumer to special-case them), we now let the
plugin system process all bindings normally, then rewrite external service
bindings and tails to point at the dev registry proxy afterward. This follows
the same post-processing pattern already used for assets at line 1673.

Removes kResolvedServiceDesignator, ResolvedServiceDesignator, the zod
validator, and checks in getCustomServiceDesignator, maybeGetCustomServiceService,
and normaliseServiceDesignator.
The proxy's get trap now returns values that are both callable (for
env.SERVICE.method()) and thenable (for await env.SERVICE.property),
matching workerd's JsRpcProperty behavior. Without the .then property,
awaiting a proxy-intercepted property would resolve to the function
itself rather than triggering remote resolution.
Debug port RPC fetchers don't support lifecycle event dispatch
(fetcher.scheduled()), so scheduled events need special handling:

- ExternalServiceProxy: forward scheduled via HTTP to the entry worker's
  /cdn-cgi/handler/scheduled endpoint, which dispatches internally
- RPCProxyWorker (assets proxy): forward scheduled to USER_WORKER via
  Fetcher.scheduled()
- Add service_binding_extra_handlers compat flag to assets proxy service
  config so Fetcher.scheduled() is available on the USER_WORKER binding
Refactors the  to be more generic, efficient,
and readable.

- Introduces a private  method that handles
  resolving and connecting to the remote service.
- Caches the remote fetcher promise for 1s to avoid re-connecting
  on every single RPC call or property access.
- Simplifies the Proxy  trap by using the new helper, removing
  duplicated code and making the logic much clearer.
@workers-devprod
Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • packages/miniflare/CONTRIBUTING.md: [@cloudflare/wrangler]
  • packages/miniflare/scripts/build.mjs: [@cloudflare/wrangler]
  • packages/miniflare/src/http/fetch.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/assets/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/explorer.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/shared/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/runtime/config/workerd.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/runtime/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/external-service.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/assets/rpc-proxy.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/dev-registry-proxy-shared.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/entry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/dev-registry.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/integrations/platform/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/utils/print-bindings.ts: [@cloudflare/wrangler]

@workers-devprod
Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • packages/miniflare/CONTRIBUTING.md: [@cloudflare/wrangler]
  • packages/miniflare/scripts/build.mjs: [@cloudflare/wrangler]
  • packages/miniflare/src/http/fetch.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/assets/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/explorer.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/shared/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/runtime/config/workerd.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/runtime/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry-types.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/external-service.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/assets/rpc-proxy.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/dev-registry-proxy-shared.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/entry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/dev-registry.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/integrations/platform/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/utils/print-bindings.ts: [@cloudflare/wrangler]

@workers-devprod
Copy link
Copy Markdown
Contributor

workers-devprod commented Mar 20, 2026

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • packages/miniflare/CONTRIBUTING.md: [@cloudflare/wrangler]
  • packages/miniflare/scripts/build.mjs: [@cloudflare/wrangler]
  • packages/miniflare/src/http/fetch.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/assets/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/explorer.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/core/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/shared/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/runtime/config/workerd.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/runtime/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry-types.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/dev-registry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/shared/external-service.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/assets/rpc-proxy.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/constants.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/dev-registry-proxy-shared.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/core/entry.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/aggregation.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/workers/local-explorer/explorer.worker.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/dev-registry.spec.ts: [@cloudflare/wrangler]
  • packages/vite-plugin-cloudflare/src/miniflare-options.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/integrations/platform/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/utils/print-bindings.ts: [@cloudflare/wrangler]

devin-ai-integration[bot]

This comment was marked as resolved.

…ocket

On Windows, the internal Cap'n Proto service-to-service forwarding from
the entry worker to the dev-registry-proxy service can break with
WSARecv error 64 (ERROR_NETNAME_DELETED), causing the registry push to
silently fail and leaving the proxy worker with an empty registry.

Give the dev-registry-proxy service its own HTTP socket (SOCKET_DEV_REGISTRY)
and have #pushRegistryUpdate POST directly to it, bypassing the entry worker
entirely. Also incorporates review fixes: reset previousJSON on restart,
guard generated identifiers, check dispose signal on retry, read fresh
registry on retry, shallow-copy workerOpts before mutation, use real entry
port for loopbackAddress, and guard malformed loopbackAddress parsing.
@edmundhung edmundhung self-requested a review March 30, 2026 12:45
@workers-devprod
Copy link
Copy Markdown
Contributor

workers-devprod commented Mar 31, 2026

Codeowners approval required for this PR:

  • ✅ @cloudflare/wrangler
Show detailed file reviewers

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

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

This looks great. I have not yet pulled it down and played with it locally.
But a code read through didn't bring up any red flags that I can find.

let text = await response.text();
if (response.status !== 200)
throw new Error(`Got ${response.status}: ${text}`);
expect(response.status).toBe(200);
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.

Not needed given the above statement.
Or was the throw just there for debugging?

name: `${RPC_PROXY_SERVICE_NAME}:${id}`,
worker: {
compatibilityDate: "2024-08-01",
compatibilityFlags: ["service_binding_extra_handlers"],
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.

???

import { Log } from "./log";
import { getGlobalWranglerConfigPath } from "./wrangler";
import type { WorkerDefinition, WorkerRegistry } from "./dev-registry-types";
export type { WorkerDefinition, WorkerRegistry } from "./dev-registry-types";
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.

duplicate of line above?

break;
}
}
const registry = getWorkerRegistry(this.registryPath!);
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.

perhaps just add an asser?

Suggested change
const registry = getWorkerRegistry(this.registryPath!);
assert(this.registryPath);
const registry = getWorkerRegistry(this.registryPath);

Comment on lines +73 to +79
this.watcher
?.close()
.then(() => {})
.finally(() => {
this.watcher = undefined;
}) ?? Promise.resolve()
);
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.

I assume that if the .then and .finally are not breaking type check then this.watcher cannot be undefined here? If so, then the ?. can be removed as well as the ?? Promise.resolve().

Suggested change
this.watcher
?.close()
.then(() => {})
.finally(() => {
this.watcher = undefined;
}) ?? Promise.resolve()
);
this.watcher
.close()
.then(() => {})
.finally(() => {
this.watcher = undefined;
})
);

If it can be undefined, then I assume what you actually need here is:

Suggested change
this.watcher
?.close()
.then(() => {})
.finally(() => {
this.watcher = undefined;
}) ?? Promise.resolve()
);
(this.watcher?.close() ?? Promise.resolve())
.then(() => {})
.finally(() => {
this.watcher = undefined;
});

"editor.formatOnSave": true
"editor.formatOnSave": true,
"[typescript]": {
"editor.defaultFormatter": "oxc.oxc-vscode"
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.

❤️

import { DurableObject } from "cloudflare:workers";

// These interfaces mirror the workerd debug port RPC API.
// See https://github.com/cloudflare/workerd/blob/main/src/workerd/server/server.c++
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.

TIL - I didn't know this even existed. Nice

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.

NIT: All the exported interfaces, classes and functions in this file would benefit from JSDocs explaining their purpose.

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.

RPC to Durable Objects should work with multiple wrangler dev sessions

3 participants