Skip to content

fix(devframe): skip ports held on another loopback address - #461

Merged
antfu merged 3 commits into
devframes:mainfrom
DBAAZzz:fix/get-port-loopback-families
Oct 30, 2026
Merged

antfu merged 3 commits into
devframes:mainfrom
DBAAZzz:fix/get-port-loopback-families

Conversation

@DBAAZzz

@DBAAZzz DBAAZzz commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

fix #460

If another server holds 127.0.0.1:<port>, getPort from get-port-please still returns that port for localhost, because it tries to bind only one address. devframe then binds [::1]:<port>, and requests to 127.0.0.1:<port> go to the other server.

This PR patches get-port-please. devframe bundles it, so the patch ships with devframe. For a loopback or wildcard host, getPort now also tries 127.0.0.1, ::1, 0.0.0.0 and ::. If one of them fails with EADDRINUSE or EACCES, the port is in use. Other errors, like EADDRNOTAVAIL when IPv6 is off, mean that the address does not exist, so they are ignored.

The new test in dev.test.ts fails on main and passes with the patch, on Linux (IPv6 on) and on macOS. I will send the same change upstream for unjs/get-port-please#121. You can remove the patch after upstream releases it.

@vercel

vercel Bot commented Oct 26, 2026

Copy link
Copy Markdown

@DBAAZzz is attempting to deploy a commit to the NuxtLabs Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

👁️‍🗨️ Review this pull request with grouped, summarized diffs at:
👉 https://pulls.review/gh/devframes/devframe/461?from=github-actions[bot]

raw result

💭 analyzed by vercel-ai-gateway/anthropic/claude-sonnet-5
🕰️ 2026-10-30 21:35 UTC
🔗 head 873529d
🤖 automated by pulls.review

{
  "headSha": "873529d6386c66e50d5e76494313900dc7f9b440",
  "result": {
    "overallSummary": "Fixes an issue where devframe's dev server could silently share a port with another process already listening on `127.0.0.1`, because `get-port-please` only probed one loopback address. The fix centralizes \"bind on a host plus its loopback twin\" logic in a new `node/listen.ts` module, wires it into `createDevServer` with a retry-on-taken-port loop (while still failing fast for an explicitly pinned port), and adds tests that reproduce and verify the fix.",
    "groups": [
      {
        "key": "loopback-port-fix",
        "label": "Loopback port binding fix",
        "summary": "Fixes a dev server port collision bug: when another process already holds `127.0.0.1:<port>`, the previous port resolution (via `get-port-please`) still reported the port as free for `localhost`, causing devframe to bind only `[::1]:<port>` while requests on `127.0.0.1` silently reached the other process. The loopback-twin bind/retry logic is extracted from `instance-shell.ts` into a shared `node/listen.ts` helper and reused by `createDevServer`, which now retries with a new port when the requested one (or its loopback twin) is actually taken, while a pinned `--port` still fails fast with `DF0052`. `cac.ts` is updated to pass the preferred port through as `defaultPort` instead of pre-resolving it.",
        "category": "core",
        "filePaths": [
          "packages/devframe/src/adapters/dev.ts",
          "packages/devframe/src/node/listen.ts",
          "packages/devframe/src/node/instance-shell.ts",
          "packages/devframe/src/adapters/cac.ts"
        ],
        "critical": true,
        "fileNotes": [
          {
            "path": "packages/devframe/src/node/listen.ts",
            "text": "New shared helper extracted from instance-shell.ts; binds the requested host and, for localhost, also binds the other loopback address, failing like a taken port if that twin bind is refused."
          },
          {
            "path": "packages/devframe/src/adapters/dev.ts",
            "text": "createDevServer now retries port resolution up to 3 times on EADDRINUSE/EACCES when the port wasn't explicitly pinned, using the new listenWithLoopbackTwin helper."
          }
        ]
      },
      {
        "key": "tests",
        "label": "Tests and snapshot",
        "summary": "Adds regression tests that reproduce the loopback port-sharing bug (one holding 127.0.0.1 before starting the dev server) and verify both that an unpinned port moves off the taken one and that a pinned port fails with DF0052; the type snapshot is updated for the new `defaultPort` option.",
        "category": "tests",
        "filePaths": [
          "packages/devframe/src/adapters/__tests__/dev.test.ts",
          "tests/__snapshots__/tsnapi/devframe/adapters/dev.snapshot.d.ts"
        ],
        "critical": true
      }
    ],
    "schemaVersion": 1,
    "source": "llm",
    "generatedAt": "2026-10-30T21:35:09.795Z",
    "model": "vercel-ai-gateway/anthropic/claude-sonnet-5",
    "locale": "en"
  }
}

antfubot and others added 2 commits October 30, 2026 21:28
…loopback-families

# Conflicts:
#	pnpm-lock.yaml
Drop the `get-port-please` patch. `createDevServer` on `localhost` now
also listens on the other loopback address, like the side-car does since
`EADDRINUSE` and a port that was not pinned moves to the next free one.
A pinned `port` / `--port` still fails with `DF0052`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antfubot

Copy link
Copy Markdown
Collaborator

I pushed two commits to this branch. The fix now works without patching get-port-please.

Why #462 did not fix this: #462 made the side-car listen on both loopback addresses. createDevServer binds its own port and does not use that code, so the test in this PR still failed on main without the patch.

What changed

  • Merged main (the only conflict was pnpm-lock.yaml).
  • Removed the patch. pnpm-lock.yaml and pnpm-workspace.yaml now match main.
  • Moved the listen helper from fix(devframe): listen on both loopback addresses for a localhost side-car #462 to packages/devframe/src/node/listen.ts, so the side-car and the dev server use the same code.
  • createDevServer on localhost now also listens on the other loopback address. If another process holds it (EADDRINUSE), the dev server tries the next port, up to 3 times. A port that you set with port or --port does not move. It still fails with DF0052.
  • Added a defaultPort option to createDevServer. The CLI now passes --port only when you set it, and passes the preferred port as defaultPort. Before, the CLI always passed a port, so the dev server could not tell if you pinned it.
  • Tests: your test now also checks that the dev server answers on 127.0.0.1 and [::1]. A new test checks that a pinned port held on 127.0.0.1 fails with DF0052.

Why not the patch: the bind itself checks the port, so there is no race between a probe and the bind. Pages on 127.0.0.1 can now reach a dev server on localhost too. And there is no patch to remove later.

Not covered: wildcard hosts like 0.0.0.0 on macOS, and other callers of devframe/utils/get-port that bind the port themselves. A fix upstream in unjs/get-port-please#121 would still help them.

CI: lint, typecheck and the devframe tests (706 of 706) pass. CI on main has been failing since ce882971 (chore: update deps): @devframes/nuxt fails to build with TS2883, knip reports @x18n/cli, and the API snapshots changed with the new build output. So CI on this PR will fail until main is fixed.

@antfu
antfu merged commit 0cf8fd5 into devframes:main Oct 30, 2026
2 of 3 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.

createDevServer starts on a port that another server holds on 127.0.0.1

3 participants