Skip to content

Pick lower ephemeral port for browser mode #9035

Closed

Description

@yjaaidi

Clear and concise description of the problem

When using browser mode, Vitest picks a port starting at 63315.
I've been observing failures with some users using "corporate" windows devices. I wasn't able to diagnose in detail but it seems that some security policies might prevent users from starting servers on high ranges.

IANA seems to recommend the following range 49152-65535 for ephemeral ports, hence the 63315, I guess.

Right now, I don't know if the whole range is blocked by such policies and if it would be more pragmatic to just use a port range <10000.

I am opening this issue to collect more feedback from the trenches.

Suggested solution

Pick ports in a lower range.

Alternative

The current workaround is to override the port manually in the configuration:

{
  test: {
    browser: {
      api: 7357
    }
  }
}

Additional context

No response

Validations

Activity

  1. added theissue type on Nov 16, 2025
  2. sheremet-va commented on Dec 22, 2025

    @sheremet-va
    Member

    I don't actually know why the current port number was selected, but I don't think there is enough information here to justify changing the default that's been there for a few years now.

  3. birtles commented on Apr 10, 2026

    @birtles

    I hit this today and running netsh interface ipv4 show excludedportrange protocol=tcp showed me that the range 63272-63371 range was reserved by something.

  4. hi-ogawa commented on Oct 14, 2026

    @hi-ogawa
    Collaborator

    Some update since this was opened. The config shape has changed. browser.api is deprecated in Vitest 5, and the port is now set with the top-level api option:

    export default defineConfig({
      test: {
        api: { port: 8315 },
      },
    })

    This seems to come up most often on Windows CI (for example facebook/lexical#8673 and voidzero-dev/vite-task#838). Windows reserves random blocks of the high port range and 63315 can land in one.

    api: { port: 0 } (let the OS pick a free port) would be the natural option, but it's currently unsupported because Vitest treats 0 as unset and falls back to the default. Fixing that seems like a good first step.

    Separately, we could reconsider the default. The browser mode port is mostly an internal connection between the Vitest server and the browser, so it doesn't need to be a fixed high port, at least when api/ui isn't requested.

    Review note: https://gist.github.com/hi-ogawa/f7a22bbc9d0565270e84fbd060b640be

  5. hi-ogawa commented on Oct 16, 2026

    @hi-ogawa
    Collaborator

    We discussed and decided on:

    • We should fix api: { port: 0 } case.
    • We can land default port number change as a fix.
    • Throw a dice or ask agent 🤖 to choose good port number
  6. added
    p3-minor-bugAn edge case that only affects very specific usage (priority)
    and removed
    p2-to-be-discussedEnhancement under consideration (priority)
    on Oct 16, 2026
  7. moved this from P2 - 2 to Approved in Team Boardon Oct 16, 2026
  8. changed the issue type fromtoon Oct 16, 2026
  9. william-xue commented on Oct 18, 2026

    @william-xue

    The api: { port: 0 } case looks like a null-vs-zero check problem rather than
    missing plumbing — on main the whole path treats 0 as "not set", so it never
    reaches Vite.

    Spots I found:

    • packages/vitest/src/node/config/resolveConfig.ts:105 — if (config.api.port)
      (object form; 0 is dropped when api/ui already populated a default)
    • packages/vitest/src/node/config/resolveConfig.ts:120 —
      if (!api.port && !api.middlewareMode) api.port = defaultPort
      This is the one that matters in browser mode: an explicit 0 gets overwritten
      with the default passed in by the caller, so the listen call never sees it.
    • packages/vitest/src/node/config/resolveConfig.ts:132 — same !api.port check
      again in the browser-forced block.
    • packages/vitest/src/node/plugins/browserLoader.ts:176 —
      if (config.api.port) await server.listen(config.api.port)
    • packages/vitest/src/node/logger.ts:245 —
      else if (this.ctx.config.api?.port && this.ctx.config._apiRequested)
      (with 0 the "API started at ..." line would be skipped)

    In browser mode that caller-side default is worth a look too: testConfig.ts:178
    passes isBrowserEnabled ? harness._browserLastPort++ : defaultPort, and
    _browserLastPort starts at defaultBrowserPort (63315) in
    pluginHarness.ts:15. So each browser project takes the next number —
    63315, 63316, ... — which is how a run can walk into a Windows reserved range
    like the 63272-63371 reported above.

    Vite itself already handles 0 end to end, which is why I think this is only the
    checks above:

    • packages/vite/src/node/http.ts:260 — if (startPort === 0) { const port = await getAvailableEphemeralPort(host); ... return port }
    • packages/vite/src/node/server/index.ts:1182-1188 — the actual bound port is
      stored on the server, and resolvedUrls is built from the real socket on
      listening.
    • Vitest already prefers the resolved URL over the configured one:
      logger.ts:249 and project.ts:495-496 use
      resolvedUrls?.local[0] ?? resolvedUrls?.network[0].

    So my expectation is that != null (or ?? ) checks in those four/five places
    are enough to let 0 flow into server.listen(0), with a test next to
    test/browser/specs/server-url.test.ts asserting that the resolved port is a
    real ephemeral port and that the browser connects to it.

    On the default number: the report above shows 63272-63371 reserved by Hyper-V on
    one machine, which is exactly what swallowed 63315 — and since that reservation is
    re-allocated on every boot, no fixed constant is safe. api: { port: 0 } looks
    like the durable fix; I'd keep the current default unless you'd rather align
    browser mode with the number you already use for the non-browser API server
    (51204). Either way works for me.

    If this direction looks right, could you add the approved label (or approve me)
    so the PR doesn't get closed by the policy workflow? I'd keep the change to those
    call sites plus one test.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    p3-minor-bugAn edge case that only affects very specific usage (priority)

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions