Skip to content

Fast vhost - #77

Open
sheplu wants to merge 5 commits into
masterfrom
fast-vhost
Open

Fast vhost#77
sheplu wants to merge 5 commits into
masterfrom
fast-vhost

Conversation

@sheplu

@sheplu sheplu commented Jun 13, 2026

Copy link
Copy Markdown
Member

v4 rewrite of vhost:

No change to the matching semantics or the req.vhost contract otherwise.

Comment thread bench/index.mjs
}

// Middlewares under test.
const staticMw = vhost('mail.example.com', handle)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the test / benchmark

Comment thread test/test.mjs
})

it('should capture a wildcard with both a prefix and a suffix', function (_, done) {
var app = createServer('user-*.example.com', function (req, res) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the test / benchmark

Comment thread test/test.mjs
})

it('should 404 a single-wildcard host with the wrong prefix', function (_, done) {
var app = createServer('user-*.example.com', function (req, res) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the test / benchmark

Comment thread test/test.mjs
})

it('should preserve wildcard case-insensitivity on prefix and suffix', function (_, done) {
var app = createServer('user-*.example.com', function (req, res) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the test / benchmark

Comment thread test/test.mjs
})

it('should treat dot as a dot', function (_, done) {
var app = createServer('a.b.com', function (req, res) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the test / benchmark

Comment thread test/test.mjs
})

it('should match case-insensitively', function (_, done) {
var app = createServer('mail.example.com', function (req, res) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the test / benchmark

Comment thread test/test.mjs
})

it('should match a mixed-case static host (fast-path regex fallback)', function (_, done) {
var app = createServer('mail.example.com', function (req, res) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

part of the test / benchmark

@UlisesGascon

Copy link
Copy Markdown
Member

@sheplu can you cheerry pick/port changes from #21 and solve the conflicts? I think is the last piece for releasing next major with this massive refactor 👍

sheplu added 4 commits August 2, 2026 18:36
Rewrite index.js as src/index.ts compiled with tsc to ESM in dist/, with
bundled type declarations. Matching logic is byte-for-byte faithful to v3.

Performance, while staying behaviour-identical:
  - Static (non-wildcard) hostnames take an ASCII-gated fast path: a length
    check + lowercase equality decide most requests without the regex, and
    req.vhost is written directly with length 0. A toLowerCase shortcut was
    rejected because RegExp i-folding differs from String#toLowerCase for code
    points like U+212A (Kelvin) and 'İ' length-changing folds; the fast path is
    gated on an ASCII pattern and defers to the regex otherwise.
  - Wildcard string hostnames gain an always-safe minimum-length reject before
    the regex (a match needs at least hostname.length chars). The regex still
    does the matching and capture extraction.
  - RegExp hostnames are unchanged.

Warm: static match ~1.37x, static no-match ~1.9x, wildcard short no-match
~2.2x vs v3; wildcard match / multi-star / RegExp stay ~1.0x.

Tooling: node:test + c8 + typescript-eslint flat config; the legacy
multi-version CI matrix is replaced with a Node 24 build-and-test job.

BREAKING CHANGE: package is now ESM-only (import, not require) and requires
Node.js 24+.
Replace the mocha suite with the built-in node:test runner (ESM) and grow it
from 19 to 56 cases to lock the behavioural contract. New cases cover:
multiple vhosts in series, null-prototype req.vhost, explicit length-0
static/RegExp shapes, case-insensitivity, regexp-metachar escaping, IPv6
with/without port, trailing-dot literals, and the optimization guards —
the Unicode case-fold (U+212A must not match /k/i), the 'İ' length-changing
fold, wildcard prefix/suffix capture + case-insensitivity, wrong-prefix and
wrong-suffix 404s, non-ASCII host via the regex path, and Kelvin in a wildcard
suffix. The module is imported via a stable package self-reference so the same
suite runs unchanged against the compiled build. 100% function coverage via c8.
Switch README examples from require() to import, note the ESM-only / Node 24+
requirement and bundled types, and document the exported VHost type. Add the
4.0.0 HISTORY entry.
Pin the original v3 implementation at bench/v3-baseline.cjs so old-vs-new stays
reproducible. Add:
  - bench/index.mjs        quick single-process snapshot (tinybench)
  - bench/session.mjs      one fresh-process session, fixed-iteration hrtime
  - bench/run-matrix.mjs   N sessions × several lengths, mean/min/max/sd
  - bench/collect.mjs      persists raw + summary per run to bench/results/
  - bench/{,run-}*experiments.mjs   variant studies, each fuzz-checked for
    identical captures against the regex (incl. the rejected wildcard string
    matcher and the object-allocation lesson)

Store a 25-session × {10k,100k,1M} run and document method + results in
BENCHMARKS.md (medians reported alongside means since OS-scheduler spikes
inflate the mean at 1M).
Route by the framework-resolved req.hostname (Express 5, respects
trust proxy / X-Forwarded-Host) when present, falling back to the raw
Host header for plain Node.js / connect servers. req.vhost.host is
populated from the same resolved value, so it reflects the host that
routing actually used.

Fixes #20, supersedes #21
@sheplu
sheplu marked this pull request as ready for review August 2, 2026 17:06
@sheplu

sheplu commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

@sheplu can you cheerry pick/port changes from #21 and solve the conflicts? I think is the last piece for releasing next major with this massive refactor 👍

@UlisesGascon this should be good!

Comment thread tsconfig.json
"moduleResolution": "NodeNext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want the source map? It will only increase the bundle size, and since we're not using any legacy features, the generated code is practically the same as the source.

Comment thread src/index.ts
Comment on lines +217 to +230
function hostnameof (host: string): string | undefined {
if (!host) {
return undefined
}

const offset = host[0] === '['
? host.indexOf(']') + 1
: 0
const index = host.indexOf(':', offset)

return index !== -1
? host.substring(0, index)
: host
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, well, it's not mentioned anywhere, but I think it's worth pointing out. For example, .example.com. and .example.com are considered equivalent, which is valid, although I still find it pretty tricky to understand (ref: expressjs/express#7331 (review)). The spec honestly just confuses me even more.

From what I understand, nothing actually requires us to reject that case. RFC 1034 says they represent the same domain, so in practice I think we should allow it as well, shouldn't we?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using express vhost behind a reverse proxy

4 participants