Skip to content

fix(plugins): Browse registry fails with 'Plugin "registry" not found' (route shadowed by /plugins/:id)#1936

Merged
gsxdsm merged 1 commit into
Runfusion:mainfrom
Automata-intelligentsia:fix/plugins-registry-route-shadowed-by-id
Jul 7, 2026
Merged

fix(plugins): Browse registry fails with 'Plugin "registry" not found' (route shadowed by /plugins/:id)#1936
gsxdsm merged 1 commit into
Runfusion:mainfrom
Automata-intelligentsia:fix/plugins-registry-route-shadowed-by-id

Conversation

@Automata-intelligentsia

@Automata-intelligentsia Automata-intelligentsia commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

In Project Settings → Plugins, the Browse registry panel fails to load with:

Failed to load registry: Plugin "registry" not found

The registry listing never appears, so one-click plugin discovery/install is unusable.

Root cause

In createApiRoutes (packages/dashboard/src/routes.ts), the generic project-scoped route GET /plugins/:id is registered before the plugin sub-router (createPluginRouter) — which owns GET /plugins/registry — is mounted (router.use("/plugins", createPluginRouter(...))).

Express matches routes in registration order, so a request to /api/plugins/registry matches :id with id === "registry", calls pluginStore.getPlugin("registry"), and throws Plugin "registry" not found. The real registry handler (which builds the curated registry manifest) is never reached.

Why not just reorder the mount?

The two handlers are not equivalent:

  • the inline GET /plugins/:id is project-scoped via getProjectContext(req),
  • the sub-router uses the global plugin store.

Mounting the sub-router first would shadow the project-scoped routes and change scoping semantics for other endpoints. So reordering is the wrong fix.

Fix

Let the reserved static path fall through. In the inline GET /plugins/:id handler, when id === "registry", call next() so the later-mounted sub-router serves the registry listing (it already handles the projectId query param). Minimal and targeted — "registry" is the only static GET path under /plugins that collides with the single-segment :id pattern.

router.get("/plugins/:id", async (req, res, next) => {
  if (req.params.id === "registry") { next(); return; }   // fall through to the registry sub-route
  ...
});

Tests

Adds a regression test in plugin-routes.routes.test.ts (using the existing createApiRoutes harness that reproduces the real mount order) asserting GET /api/plugins/registry:

  • returns 200 with a plugins array, and
  • never calls pluginStore.getPlugin("registry") (i.e. is no longer shadowed by :id).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed the /plugins/registry endpoint so it responds correctly instead of being treated like a plugin ID.
    • Improved route handling to ensure the registry page/API is reached even when generic plugin routes are registered first.
  • Tests

    • Added regression coverage for the /plugins/registry route to verify the correct response and prevent the generic plugin lookup from intercepting it.

The Plugins settings 'Browse registry' feature failed with
'Failed to load registry: Plugin "registry" not found'.

Root cause: in createApiRoutes (routes.ts), the generic project-scoped
'GET /plugins/:id' route is registered BEFORE the plugin sub-router
(createPluginRouter) that owns 'GET /plugins/registry' is mounted. Express
matches in registration order, so a request to /api/plugins/registry matched
':id' with id='registry', called pluginStore.getPlugin('registry') and threw
'Plugin "registry" not found' — the real registry handler was never reached.

Reordering the mount is unsafe: the inline ':id' route is project-scoped via
getProjectContext, while the sub-router uses the global plugin store, so
moving it would change scoping semantics. Instead, let the reserved static
path fall through: when id === 'registry', call next() so the mounted
sub-router serves the registry listing (which already handles projectId).

Adds a regression test asserting GET /api/plugins/registry returns 200 with a
plugins array and never calls getPlugin('registry').
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The GET /plugins/:id route handler in routes.ts now checks if the id parameter equals "registry" and calls next() to defer handling to the dedicated registry sub-route. A new regression test verifies this route-shadowing fix.

Changes

Route-shadowing fix

Layer / File(s) Summary
Registry route bypass and regression test
packages/dashboard/src/routes.ts, packages/dashboard/src/__tests__/plugin-routes.routes.test.ts
The GET /plugins/:id handler now calls next() and returns early when id is "registry", letting the dedicated registry route handle it; a new test confirms /api/plugins/registry returns a plugins array and that getPlugin is not called with "registry".

Estimated code review effort: 1 (Trivial) | ~5 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant PluginsIdRoute as GET /plugins/:id
    participant PluginsRegistryRoute as GET /plugins/registry

    Client->>PluginsIdRoute: GET /api/plugins/registry
    PluginsIdRoute->>PluginsIdRoute: id === "registry"?
    PluginsIdRoute->>PluginsRegistryRoute: next()
    PluginsRegistryRoute->>Client: plugins array response
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the routing bug affecting registry browsing and the /plugins/:id shadowing fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the dashboard plugin registry route shadowing. The main changes are:

  • Adds a fall-through guard for the reserved /plugins/registry path.
  • Leaves other /plugins/:id lookups on the project-scoped path.
  • Adds a regression test for the real API route mount order.

Confidence Score: 4/5

The changed route looks mergeable after checking the optional plugin-router path.

  • The intended registry request now reaches the later plugin router when that router is mounted.
  • A setup without the optional plugin router now falls through before trying a scoped plugin lookup for ID registry.

packages/dashboard/src/routes.ts

Important Files Changed

Filename Overview
packages/dashboard/src/routes.ts Adds a reserved-path fall-through in the project-scoped plugin lookup route.
packages/dashboard/src/tests/plugin-routes.routes.test.ts Adds regression coverage for /api/plugins/registry using the API route harness.

Reviews (1): Last reviewed commit: "fix(plugins): stop /plugins/:id from sha..." | Re-trigger Greptile

Comment on lines +3478 to +3481
if (req.params.id === "registry") {
next();
return;
}

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.

P2 Unmounted Registry Route Falls Through

When createApiRoutes is used without both pluginStore and pluginLoader, the plugin sub-router is not mounted, but this new branch still calls next() for /plugins/registry. That changes the old behavior from a scoped plugin lookup into a fall-through 404/error path, so any runtime or test harness that does not install the optional plugin router loses the ability to resolve a real plugin whose ID is registry.

Context Used: AGENTS.md (source)

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dashboard/src/routes.ts`:
- Around line 3478-3481: The special-case in createPluginRouter() for
req.params.id === "registry" currently falls through to next(), which makes
/api/plugins/registry unreachable when only pluginStore is configured. Update
the routing so the registry endpoint is mounted independently of pluginLoader,
or add an explicit fallback handler in the pluginStore-only path; use the
createPluginRouter() and req.params.id check to locate the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c4dbed7c-dea8-43d0-a6f2-84de98239570

📥 Commits

Reviewing files that changed from the base of the PR and between ca84473 and 8676913.

📒 Files selected for processing (2)
  • packages/dashboard/src/__tests__/plugin-routes.routes.test.ts
  • packages/dashboard/src/routes.ts

Comment on lines +3478 to +3481
if (req.params.id === "registry") {
next();
return;
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether pluginStore can be configured without pluginLoader anywhere in the codebase.
rg -nP '\bpluginStore\s*:' --type=ts -C2 packages | rg -v pluginLoader

Repository: Runfusion/Fusion

Length of output: 16644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the API route setup and plugin route mounting conditions.
rg -n -C 3 "pluginLoader|pluginStore|GET /plugins/registry|createApiRoutes|createPluginRouter" packages/dashboard/src/routes.ts packages/dashboard/src -g '!**/dist/**' -g '!**/build/**'

# Find the options type / callers for createApiRoutes.
rg -n -C 3 "createApiRoutes\(" packages/dashboard/src packages -g '!**/dist/**' -g '!**/build/**'

Repository: Runfusion/Fusion

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant route setup and registry handler.
sed -n '3440,3515p' packages/dashboard/src/routes.ts
echo '---'
sed -n '4828,4885p' packages/dashboard/src/routes.ts
echo '---'
sed -n '1038,1105p' packages/dashboard/src/routes.ts

# Show the test that covers the "plugin loader is not available" case.
sed -n '580,620p' packages/dashboard/src/__tests__/plugin-routes.routes.test.ts

Repository: Runfusion/Fusion

Length of output: 8160


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect server options and how pluginLoader/pluginStore are wired in the real app.
rg -n -C 3 "interface ServerOptions|type ServerOptions|pluginLoader\\?|pluginStore\\?|createApiRoutes\\(store, \\{" packages/dashboard/src/routes.ts packages/dashboard/src/server.ts packages/dashboard/src -g '!**/dist/**' -g '!**/build/**'

# Focus on the registry-related route tests and any no-pluginLoader cases.
rg -n -C 4 "plugins/registry|No pluginLoader|pluginLoader is not available|pluginStore," packages/dashboard/src/__tests__ packages/dashboard/src/routes/__tests__ -g '!**/dist/**' -g '!**/build/**'

Repository: Runfusion/Fusion

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the plugin sub-router and the registry route it owns.
sed -n '320,430p' packages/dashboard/src/plugin-routes.ts
echo '---'
sed -n '300,360p' packages/dashboard/src/__tests__/plugin-routes.routes.test.ts
echo '---'
sed -n '1220,1260p' packages/dashboard/src/__tests__/plugin-routes.test.ts

Repository: Runfusion/Fusion

Length of output: 7584


Keep /plugins/registry reachable without pluginLoader. createPluginRouter() is mounted only when both pluginStore and pluginLoader are set, so this next() turns /api/plugins/registry into a plain Express 404 in pluginStore-only setups. Mount the registry route separately or add an explicit fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/src/routes.ts` around lines 3478 - 3481, The special-case
in createPluginRouter() for req.params.id === "registry" currently falls through
to next(), which makes /api/plugins/registry unreachable when only pluginStore
is configured. Update the routing so the registry endpoint is mounted
independently of pluginLoader, or add an explicit fallback handler in the
pluginStore-only path; use the createPluginRouter() and req.params.id check to
locate the fix.

@Automata-intelligentsia

Copy link
Copy Markdown
Contributor Author

Heads-up on scope: this fixes a real latent route-ordering bug (/plugins/:id shadowing /plugins/registry), but it is not sufficient to fix the user-facing "Browse registry" failure in the desktop (Electron) app. There, the plugin sub-router that owns /plugins/registry is never mounted because the desktop runtime calls createServer(...) without pluginStore/pluginLoader (see #1937). With this change, the desktop path would return a 404 for /plugins/registry instead of the misleading Plugin "registry" not found — cleaner, but the registry still won't load until the loader is wired per #1937. This addresses @coderabbitai's note about the pluginStore-only path. Happy to keep this as the isolated ordering fix and let #1937 track the root cause.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@Automata-intelligentsia Thanks for the clarification — that makes sense. This PR correctly fixes the route-ordering bug (/plugins/:id shadowing /plugins/registry), and confirms that on the server side the registry sub-router now gets a chance to handle the request. For desktop/Electron, since createServer(...) is invoked without pluginStore/pluginLoader, the sub-router isn't mounted at all, so the behavior there just changes from a misleading "Plugin not found" error to a 404 — still broken, but for a different, already-tracked reason (#1937).

Keeping this PR scoped to the isolated ordering fix and letting #1937 handle the loader wiring sounds like the right call. Thanks for the walkthrough.


✏️ Learnings added
Learnt from: Automata-intelligentsia
URL: https://github.com/Runfusion/Fusion/pull/1936

Timestamp: 2026-07-07T02:36:09.854Z
Learning: In packages/dashboard, the desktop (Electron) runtime invokes `createServer(...)` without passing `pluginStore`/`pluginLoader`. As a result, the plugin sub-router that handles `GET /plugins/registry` is never mounted on desktop, so fixing route ordering between `GET /plugins/:id` and `GET /plugins/registry` (as done in PR `#1936`) is not sufficient to fix "Browse registry" on desktop — it will 404 instead of erroring, but registry loading still requires wiring the plugin loader, which is tracked in issue `#1937`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@gsxdsm gsxdsm merged commit 73da9c1 into Runfusion:main Jul 7, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants