router icon indicating copy to clipboard operation
router copied to clipboard

Support multiple hosts for routing without use of RegExp

Open EdanKriss opened this issue 1 year ago • 0 comments

I use Koa @koa/router to serve multiple routers from the same webserver. Each router serves one or more domains, sometimes including specific subdomains.

My suggestion is to enhance the type signature of Router.RouterOptions["host"] to support an array of strings, matched by equality to any member, in addition to the current single string equality and RegExp test.

I suppose the source code change would look like this: (line 771 in /lib/router.js)

Router.prototype.matchHost = function (input) {
  const { host } = this;

  if (!host) {
    return true;
  }

  if (!input) {
    return false;
  }

  if (typeof host === 'string') {
    return input === host;
  }

  if (typeof host === 'object' && host instanceof RegExp) {
    return host.test(input);
  }

+  if (Array.isArray(host)) {
+    return host.includes(input);
+  }
};

With the proposed change, the 'parent' koa app would only have the responsibility of providing an array of valid domains:

const router = new Router({
    host: [ 
        'some-domain.com',
        'www.some-domain.com',
        'some.other-domain.com',
    ],
});

I can currently accomplish similar results 2 separate ways, neither of which is as simple as I would like:

Workaround 1.

Build a dynamic RegExp from config at runtime, and assign to Router.RouterOptions["host"] - requires escaping period characters from domain - The ECMA proposal to ease this is still in infancy: https://github.com/tc39/proposal-regex-escaping - cumbersome to handle subdomains without using wildcards or accidental performance pitfall - pseudo example of my usage:

const domains = [ 
        'some-domain.com',
        'www.some-domain.com',
        'some.other-domain.com',
];
const router = new Router({
    host: new RegExp(`^(?:${domains.map(escapePeriods).join('|')})$`),
});

Workaround 2.

Execute router middleware conditionally, only if domain matches manual check - Described generically here: https://github.com/koajs/examples/blob/master/conditional-middleware/app.js - here is pseudo example of my usage:

constructor() {
    this.#examplePortalRoutes = examplePortalRouter.routes();
    this.#examplePortalAllowedMethods = examplePortalRouter.allowedMethods({ throw: true });
    this.#app.use(this.#catchErrors);
    this.#app.use(this.#domainLookup);
    this.#app.use<DomainConfigState>(this.#appRouter);
}

#domainLookup: Koa.Middleware<SettableDomainConfigState> = async (ctx, next) => {
    const domainConfig: DomainConfig | undefined = this.#domainConfigMap.get(ctx.request.hostname);
    if (domainConfig) {
        ctx.state.domainConfig = domainConfig;
        await next();
    } else {
        throw { message: `Unrecognized domain: ${domain}`, status: 400 } satisfies Errors.InvalidRequest;
    }
}

#appRouter: Koa.Middleware<DomainConfigState, Router.RouterParamContext> = async (ctx, next) => {
    switch (ctx.state.domainConfig.domainApp) {
        case 'example_portal':
            await this.#examplePortalRoutes.call(this, ctx, async () => {
                await this.#examplePortalAllowedMethods.call(this, ctx, next);
            });
            break;
    
        default:
            await next();
            break;
    }
}

Checklist

  • [x] I have searched through GitHub issues for similar issues.
  • [x] I have completely read through the README and documentation.

EdanKriss avatar Apr 09 '23 04:04 EdanKriss