fix(client-connection): resolve webServer from Connection, not the RPC caller (PRs appear disabled — fix branch attached) #6513
Replies: 3 comments
|
Thanks for the thorough write-up — I verified the root cause against current master ( Where it breaks on master:
I also fetched and reviewed your fix commit
Caveat: I reviewed the diff against master's code paths but have not built or run the suite with it applied, so treat this as a structural review, not a green build. One process note: upstream master shows regular PR merges (e.g. |
|
Thanks for taking the time to trace through this, @PerryLink — glad the root-cause analysis and the fix line up with what you're seeing in the code paths. Since PRs are disabled on this repo, what's the preferred path to get something like this merged? Happy to:
Let me know what works best on your end. |
|
Good question, and I can now give you the authoritative answer: this repo does not accept external PRs at the moment — CONTRIBUTING.md states it directly ("We are sorry that we cannot accept external pull requests at the moment", CONTRIBUTING.md:9), and the repo has Issues/PRs turned off in favor of Discussions (I checked: the issues feature is disabled and the pulls API returns 404 for this repository). To be clear about my role: I'm a community contributor like you, not a maintainer — I can verify and document, but I can't merge anything either. So the realistic path for your fix:
Nothing further needed from me on the code side — the shape, duplicate-channel behavior, and the injected-runner test seam were all consistent with this codebase's conventions. |
Uh oh!
There was an error while loading. Please reload this page.
Summary
A third-party plugin that registers an RPC endpoint through the public, carrier-neutral
connection.rpc.handle()API silently fails to mount its route in a Web composition, because the Host implementation resolveswebServerfrom the calling plugin's own context instead of from Connection's own context. I found the root cause, fixed it, and added regression coverage. Pull requests appear to be disabled on this repository, so I'm posting the fix here instead, with the full diff on a public branch for a maintainer to cherry-pick or pull directly.Branch (fork): https://github.com/SergioMazariego/deepseek-harness/tree/fix/client-connection-external-rpc-web
Commit:
118e6c9be8bd4408fc965d7826864d0fb2dc0a0bCompare view: master...SergioMazariego:deepseek-harness:fix/client-connection-external-rpc-web
Regression
The endpoint belongs to a third-party plugin that authenticates providers via subscriptions/OAuth instead of API keys. It registers its RPC endpoints through Connection's public API (
connection.rpc.handle(...)), declaring onlyinject: ['connection']— the documented contract for that API, which is meant to be carrier-neutral.Root cause
HostConnectionService.register()(the implementation behindconnection.rpc.handle(), inpackages/client/connection/src/rpc-host.ts) mounted the channel's route by callingowner.webServer.register(route)directly onowner, the calling plugin's own context (get rpc()capturesowner = this.ctx, rebound per caller — see the class doc: "whose channel registrations belong to the caller fiber").Cordis resolves a service reference by walking the accessing fiber's own ancestor chain; a fiber that never crosses a
webServer-providing ancestor getscannot get property "webServer" without inject. A plugin that only declaresinject: ['connection']— exactly what the public API asks for — has no such path towebServerwheneverwebServeris provided by its own separate plugin fiber rather than directly on the root context (the shape of a normal Web composition, and the shape the existing test suite happened not to exercise: itsmounted()helper calledctx.provide('webServer', …)directly on the literal root, which masks the bug).So the very first call to
connection.rpc.handle()from such a plugin throws synchronously inside the registration effect. That failure is local to the registering plugin's own fiber, so the rest of the app keeps running — but the plugin's channel is never mounted. A later request to that channel's path finds no route, falls through to the webServer's fallback seat, and — under the shipped Web composition, where the fallback isfrontend-static— receives HTTP 405, becausefrontend-static's fallback answers any non-GET/HEAD request with 405 outside its own named routes (packages/host/frontend-static/src/index.ts).This is not a new dependency requirement working as intended:
connection.rpc.intercept('/api', …)andconnection.fetch.register(...)— the other two RPC/Fetch registration APIs on the same service — never touchwebServerat all; they only touch Connection's own internal maps and piggyback on the one/apiroute Connection's ownapply()already mounts through its existing optionalctx.inject(['webServer'], …).rpc.handle()was the one path that bypassed that abstraction and reached for the caller's ownwebServeraccess instead.Fix
HostConnectionServicenow resolveswebServeritself, through its own soft dependency declared in its constructor (ctx.inject(['webServer'], …), on Connection's own context — the same context that already optionally mounts/api), instead of requiring the calling plugin's context to reach it:register()records the channel'sWebRoutein an internal map (channelRoutes) and mounts it immediately ifwebServeris already active.ctx.inject(['webServer'], …)callback mounts every already-recorded channel wheneverwebServer(re)activates, and withdraws every mounted channel whenwebServerstops (a restart, or an isolated realm unloading).rpc.handle()'s return value) still belongs to the caller's own fiber — removing the channel's registration when that plugin unloads — preserving the class's existing "channel registrations belong to the caller fiber" contract. Only the route-mounting step moved off the caller's context.This is the minimal change: no changes to
registerInterceptor/registerFetchRoute(already carrier-neutral), no changes to the publicHostConnectionRpc/HostConnectionFetchtypes, andclient-connection's own top-levelinject = ['credentials']is untouched —webServerstays optional for the whole plugin, exactly as the existing doc comment describes ("Provides carrier-neutral RPC and Fetch registries. WhenwebServeris present, the plugin also mounts the/apibrowser transport"), so Desktop, headless, and other webServer-less compositions are unaffected.Tests
packages/client/connection/tests/external-rpc-web.host.spec.ts:webServeras its own nested plugin fiber (not directly on the root context, which is what let the existing suite miss this) alongsideclient-connection, then a sibling plugin declaring onlyinject: ['connection']that callsconnection.rpc.handle(...). Asserts the route mounts, a real request reaches the handler (not a 405), and disposing the external plugin withdraws the route.webServerexists anywhere in the tree, asserts nothing is mounted yet, then activateswebServerand asserts the channel mounts; disposingwebServerwithdraws the route without disturbing the external plugin's own registration.cannot get property "webServer" without inject, and pass with the fix.packages/client/connection/tests/node-half.host.spec.ts): the duplicate-channel error is now raised byHostConnectionServiceitself (already registered) rather than by (in that test) the fake webServer's own duplicate-route check.packages/client/connectionsuite (165 tests) andpackages/host/webserver+packages/host/frontend-static(5 tests): all green.tsc -b packages/client/connection/tsconfig.host.jsonandtsconfig.client.json: no errors.oxlintover the changed files: 0 warnings, 0 errors.rpc-host.ts(this repo's per-file coverage gate) with the new test alone.Manual verification
That workaround happened to work because it moved
client-connection's own base fiber onto a Web composition wherewebServerwas declared close enough in the tree for the caller's own context to resolve it in that particular installation's plugin layout — it does not hold in general (e.g. it cannot help a plugin mounted where no ancestor of its own fiber ever provideswebServer), and it makeswebServermandatory for the wholeclient-connectionplugin, which is not architecturally correct (seepackages/client/connection/src/index.ts's doc comment). The fix on the branch above resolveswebServerfrom Connection's own context unconditionally, so it works regardless of where aconnection.rpc.handle()caller sits in the plugin tree, and keepswebServeroptional forclient-connectionas a whole.Note on delivery
I tried opening a pull request first (
fix/client-connection-external-rpc-webon my fork,SergioMazariego/deepseek-harness, againstmaster), but this repository has Pull Requests disabled at the repository-feature level (confirmed via the API:has_pull_requests: false— listing existing PRs also 404s), so posting here per the README's contribution guidance. Happy to adjust the change, split it, or answer questions — the branch above stays up to date if I need to push a revision.All reactions