fix(channels): rebind inbound route handler on hot-reinstall (#395)#407
Open
pkalusek wants to merge 1 commit into
Open
fix(channels): rebind inbound route handler on hot-reinstall (#395)#407pkalusek wants to merge 1 commit into
pkalusek wants to merge 1 commit into
Conversation
…#395) ExpressRouteRegistry mounted a fresh Express route on every register(). A channel plugin hot-reinstall re-runs activate() -> core.registerRoute(), which pushed a second app.<method>(path) for the same path. Express dispatches in registration order, so the first wrapper -- closing over the old module's handler -- kept winning. Inbound traffic (e.g. Teams /api/messages, Adaptive-Card Action.Submit) silently ran stale code until a full process restart, while the proactive sender hot-swapped correctly. Mount each route/router once and store the handler in a mutable per-route record; re-registration rebinds in place instead of stacking a shadowed mount. A different channel claiming an already-owned path is rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Channel plugin hot-reinstalls now rebind the inbound HTTP handler to the freshly-loaded module instead of silently serving stale code.
ExpressRouteRegistry.register()mounted a fresh Express route on every call. A hot-reinstall re-runs the plugin'sactivate()→core.registerRoute(...), which pushed a secondapp.<method>(path)for the same path. Express dispatches in registration order, so the first wrapper — closing over the old module's handler — kept winning. Inbound traffic (e.g. Teams/api/messages, Adaptive-CardAction.Submit) ran stale code until a full process restart, while the proactive sender (keyed + disposed viaNotificationRouter) hot-swapped correctly — the split-brain reported in #395.Why
A reinstall appeared to take (cards/UI updated) but inbound behaviour silently ran the previous module. It was easy to mis-diagnose as a parsing/contract bug rather than a stale-module bug, and the only fix was
fly machine restart.How it works
The registry now keys routes in a
Map<"${method} ${path}", RouteMount>and mounts each route/router once. The Express wrapper closes over a mutable per-route record and readsmount.handleron each dispatch, so a same-channel re-registration rebinds in place (logged asroute rebound …) rather than stacking a shadowed mount. Every hot-swap path (config reactivate, version upload) deactivates then re-activates the channel, which re-runsactivate()and lands on this rebind branch.registerRoutergets the same treatment symmetrically, and a different channel claiming an already-owned path/prefix is now rejected with a throw instead of silently transferring ownership.describe()reports one entry per path regardless of re-registration count.Tests
middleware/test/channelRouteRebind.test.ts(4 cases, real Express +fetch): rebind serves the reinstalled handler, no second mount is stacked, cross-channel ownership is rejected, and a deactivated channel is gated with 503 until re-activation. Passes 4/4.ExpressRouteRegistry,createCoreApi,NotificationRouter): pre-fix the inbound path reportsv1(stale) while the proactive sender reportsv2; post-fix both reportv2.eslintclean on the changed file.Backward compatibility
Behaviour-preserving for the normal register-once path. No manifest, API, env-var, or schema changes.
Closes #395