Replies: 2 comments
|
This is the same bug already being worked through in #6227, and I think I can explain why your Attempt 2 still throws while the fix verified there does not, worth cross-checking. Confirmed your core diagnosis against current master first: packages/client/connection/src/rpc-host.ts's rpc getter does const owner = this.ctx, and HostConnectionService is constructed with the plugin's own apply-time ctx (packages/client/connection/src/index.ts), which only declares inject = ['credentials']. Exact match. On why Attempt 2 (owner.inject(['webServer'], webCtx => webCtx.effect(...)) called fresh, inside register()) still fails while #6227's verified fix does not: the structural difference is when the inject() call happens, not just what it looks like. In #6227's working fix, ctx.inject(['webServer'], webCtx => { connection.webCtx = webCtx; ... }) is called exactly once, synchronously, during the connection plugin's own apply() — the same call already used for the /api route. register() never calls inject() itself; it just reads the already-captured webCtx and does (this.webCtx ?? owner).webServer.register(route) inside owner.effect(). No new inject() call happens at request time. Your Attempt 2 calls owner.inject(['webServer'], ...) fresh, inside register(), which by the time it runs is deep inside a call chain that originated from a completely different plugin's own later, dynamic ctx.inject(['connection'], ...) callback — well after both plugins' synchronous apply() phases finished. Cordis's inject/fiber model (I read through vendor/cordis/src/reflect.ts's property guard to check this) walks a fiber's declared parent chain that's established as part of the plugin composition graph. A fresh inject() called ad hoc, long after apply(), from inside another plugin's own nested runtime callback, may not produce a fiber wired into that graph the same way a same-tick apply()-time inject() does. That would explain why the property-access guard cannot find webServer anywhere up the chain for your webCtx, even though the callback itself did fire (confirmed by your own diagnostics). If that's right, the practical implication is: your case (calling connection.rpc.handle() from inside your own plugin's ctx.inject(['connection'], ...) callback) should actually be fixed by #6227's approach too, since that fix removes the need for register() to call inject() at all, it just reuses a webCtx that was already correctly wired during the connection plugin's own apply(). Worth trying wsxwj123's exact patch from #6227 against your repro rather than your own Attempt 2, since the two aren't equivalent despite looking similar. Might be worth linking these two threads together either way, since they're clearly the same root cause. |
|
Update: the third-party plugin ( Also tried applying @wsxwj123's verified patch from #6227 directly to So it looks like plugin authors following the "inject webServer yourself, then pass that context as owner" workaround (as documented in #6227) is the reliable fix from the plugin side today, whether or not the framework-side |
Uh oh!
There was an error while loading. Please reload this page.
Summary
HostConnectionService.register()(the implementation behind the publicconnection.rpc.handle(channel, handler)API) accessesthis.ctx.webServerdirectly. Butthis.ctxis the Context captured whenHostConnectionServicewas constructed insidedsh-client-connection's ownapply()— a Context that was never injected withwebServer. Only a separate, laterctx.inject(["webServer"], webCtx => ...)callback inside the sameapply()function (used for mounting the fixed/apiroute) actually haswebServeraccess.As a result, every third-party plugin that calls the documented public API
connection.rpc.handle(channel, handler)to register its own RPC channel throws at registration time:This breaks the plugin's channel silently: the plugin's own
try/catcharound the outerctx.inject(["connection"], ...)call does not see this error (it's thrown inside a nested nested nestedeffect/injectcallback that is not run synchronously — see reproduction below), so the plugin logs no error and appears to have registered successfully. The channel is simply never mounted. Any HTTP request to that channel's path then falls through to the SPA static-file fallback route (dsh-host-frontend-static), which returns 405 for any non-GET/HEAD method instead of reaching the plugin's handler.Environment
@deepseek-ai/dsh:0.1.5-rc.1@deepseek-ai/dsh-client-connection:0.1.5-rc.1@deepseek-ai/dsh-host-webserver:0.1.5-rc.1dsh-plugin-subscriptions@0.8.0(https://github.com/V1ki/dsh-plugin-subscriptions), which callsconnection.rpc.handle('/subscriptions-auth', handler, { authority: 'loopback' })from inside its ownctx.inject(["connection"], ctx => { ... })callback — exactly the pattern the package's own doc comment describes as supported (register(owner, channel, handler)takes an arbitrary calling Context asowner).This is very likely not specific to this one plugin — any plugin using the public
dsh-client-connectionRPC registration API from its own deferred/injected context will hit the same failure, because the bug is inHostConnectionService.register()itself, not in the third-party plugin.Reproduction
ctx.inject(["connection"], (ctx) => { const connection = ctx.get("connection"); connection.rpc.handle("/my-channel", handler); }).dsh webnormally (no special config needed).POSTrequest to/my-channel/<endpoint>with a validclient-requestenvelope.GETto the same path: observe HTTP 404 — confirming the SPA static-file fallback (dsh-host-frontend-static) is answering, not the plugin's route, because the route was never registered indsh-host-webserver's route table.Confirmed via added diagnostics (this session)
Adding
process.stderr.write(...)diagnostics directly insidedsh-client-connection'sregister()method showed:Notably: even inside the callback that
ctx.inject(["webServer"], webCtx => ...)hands back (wherewebCtxshould, by definition, carrywebServeraccess), callingwebCtx.webServer.register(route)inside a further nestedwebCtx.effect(...)call still throws "cannot get property webServer without inject". This suggests the failure is not simply "the wrong Context object was captured" (the originally-suspected bug —HostConnectionService.ctxnever being updated to thewebServer-carrying Context created atapply()time) but something about how Cordis's service-access guard interacts withinject/effectcall chains that cross plugin boundaries and are nested several levels deep (subscribing plugin'seffect→dsh-client-connection'sregister()→ its owninject→ its owneffect).Original (unmodified) code path, for reference:
Two fix attempts tried this session (both insufficient — documenting for maintainers)
Attempt 1 — reassign
connection.ctx = webCtxinside theapply()-levelctx.inject(["webServer"], ...)callback, so later calls toregister()(from any plugin) see a Context that haswebServer. Not fully tested before attempt 2 looked more correct; abandoned due to an inherent race (no guaranteeregister()is called from third-party plugins strictly after this reassignment runs).Attempt 2 — change
register()itself to defer via its ownowner.inject(["webServer"], webCtx => webCtx.effect(() => webCtx.webServer.register(route), ...)), mirroring the pattern already used successfully for the/apiroute insideapply(). This did avoid the immediate synchronous throw (the outerctx.inject(["connection"], ...)callback in the calling plugin no longer sees an exception), but the innerwebCtx.webServer.register(route)call still throws the same "cannot get property webServer without inject" error, confirmed viafiber.then(onFulfilled, onRejected)on the returned Fiber. See stack trace above.This means the bug is deeper than "the captured Context lacks webServer" — even a freshly-injected
webCtxfails when accessed from inside a nestedeffect()that itself originates from a call chain crossing from a different plugin's owneffect. Both attempted fixes were reverted; the shipped0.1.5-rc.1code is currently unmodified in this environment.Impact
Any plugin's Settings/Subscriptions-style RPC channel (registered via the public
connection.rpc.handle()API from the plugin's own injected context) silently fails to mount. The plugin appears to load without error; only a live HTTP probe reveals the channel is unreachable (405 for POST, 404 for GET). This is a functional regression for third-party plugin authors following the documented/supported registration pattern, and is likely to affect other plugins beyond the one used to reproduce it here.Suggested areas to investigate
"cannot get property ... without inject") is scoped per-Fiber rather than per-Context-object, and whether a Context returned byinject()genuinely carries a usable access grant when the code accessing it runs inside a further nestedeffect/injectinitiated from outside the plugin that originally calledinject().HostConnectionService.register()needs to capture/refresh its Context differently, or whetherdsh-client-connectionneeds to expose a different public API shape that plugins are expected to use instead of directly callingowner.effect(() => owner.webServer.register(route))with anownerthe plugin does not fully control the injection state of.All reactions