Skip to content

fix(router): use instanceof Router instead of constructor.name for sub-router detection#354

Open
jcpsimmons wants to merge 1 commit into
kartikk221:masterfrom
customerio:fix/sub-router-instanceof
Open

fix(router): use instanceof Router instead of constructor.name for sub-router detection#354
jcpsimmons wants to merge 1 commit into
kartikk221:masterfrom
customerio:fix/sub-router-instanceof

Conversation

@jcpsimmons

Copy link
Copy Markdown

Problem

The sub-router detection in Router.js uses candidate.constructor.name === 'Router':

} else if (typeof candidate == 'object' && candidate.constructor.name === 'Router') {
    // Scenario: Router instance
    this._register_router(pattern, candidate);
}

This is fragile to bundler/minifier class renaming. Modern JS bundlers and minifiers commonly rename class bodies during compilation, in which case .name no longer returns "Router" and sub-router mounting silently fails: every app.use(pattern, router) call becomes a no-op and the routes registered on the sub-router return 404.

Concrete failure case

esbuild 0.25+ emits class _Router {} when bundling this library as a transitive dependency, which changes .name to "_Router" and breaks the check. We hit this in production after an esbuild upgrade. The only viable workaround was keepNames: true, which restores .name but injects __name() wrappers around every function and class in the bundle. At our scale (thousands of classes in the bundle) this cost ~20% production CPU on the affected service.

Fix

Swap the check for candidate instanceof Router:

} else if (candidate instanceof Router) {
    // Scenario: Router instance
    this._register_router(pattern, candidate);
}

instanceof walks the prototype chain to test class identity, so it works regardless of how the class is renamed in compiled output. Same semantic check, just done in a way that survives compilation.

Compatibility

  • No public API change
  • No behavior change for any caller passing a real Router instance
  • The previous check would also match any object whose constructor was named "Router" coincidentally (e.g. another library's Router class). instanceof is stricter and only matches actual HyperExpress Router instances, which is the intended semantic

Why this matters even if you have no plans to ship bundled

People bundle their HyperExpress apps. esbuild, webpack, Rollup, swc, etc. all touch class names during compilation in ways that can break .name-based dispatch. Making the internal detection robust to that is a one-line change that prevents a hard-to-debug silent failure for downstream users.

(I appreciate the project may be in maintenance mode — leaving this PR here so the fix is documented and available to anyone else who hits the same issue, even if it's not merged.)

…b-router detection

The current check `candidate.constructor.name === 'Router'` is fragile to
bundler/minifier class renaming. Modern JS bundlers and minifiers commonly
rename class bodies during compilation, in which case `.name` no longer
returns "Router" and sub-router mounting silently fails (every
`app.use(pattern, router)` call becomes a no-op).

Concrete example: esbuild 0.25+ emits `class _Router {}` when bundling
this library as a transitive dependency, breaking all sub-router routing
in the output bundle. The author of this PR's company hit this in
production after an esbuild upgrade and had to enable `keepNames: true`
as an expensive workaround (it injects `__name()` wrappers around every
function and class in the bundle).

`instanceof Router` walks the prototype chain to test class identity, so
it works regardless of how the class is renamed in compiled output. It's
the same semantic check, just done in a way that survives compilation.

No API change, no behavior change for callers; only the internal detection
becomes more robust.
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.

1 participant