diff --git a/CHANGELOG.md b/CHANGELOG.md index 5900b912c..399f68d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### An empty app port is the default, not a random one + +`APP_PORT=` and `SERVER_PORT=` in a compose file or leftover `.env` used to become `NaN` for the Vite +dev and preview servers, so the UI bound an ephemeral port while the proxy target was `http://localhost:`. +Both empty values now mean the documented defaults (3010 and 3001), and a non-numeric value refuses to start. + ## 0.0.8 ### A desktop shell that installs OpenBot and then becomes it diff --git a/app/tsconfig.json b/app/tsconfig.json index 8775fe297..0c68ed165 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -8,5 +8,10 @@ "jsx": "react-jsx", "lib": ["ES2024", "DOM", "DOM.Iterable"] }, - "include": ["src", "vite.config.ts", "../shared/handoff-markers.ts"] + "include": [ + "src", + "vite.config.ts", + "../shared/handoff-markers.ts", + "../shared/listen-port.ts" + ] } diff --git a/app/vite-listen-port.test.ts b/app/vite-listen-port.test.ts new file mode 100644 index 000000000..5e54d63b8 --- /dev/null +++ b/app/vite-listen-port.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { listenPort } from "../shared/listen-port"; + +/** + * The app Vite config used `Number.parseInt(process.env.APP_PORT ?? "3010", 10)` (and the same + * `??` for SERVER_PORT in the proxy target). Empty compose / leftover `.env` lines are `""`, so + * parseInt was NaN and the proxy URL was `http://localhost:`. Same helper the Bots already use. + */ +describe("app listen ports", () => { + test("empty APP_PORT is 3010", () => { + expect(listenPort(undefined, 3010)).toEqual({ ok: true, port: 3010 }); + expect(listenPort("", 3010)).toEqual({ ok: true, port: 3010 }); + expect(listenPort(" ", 3010)).toEqual({ ok: true, port: 3010 }); + }); + + test("empty SERVER_PORT is 3001", () => { + expect(listenPort("", 3001)).toEqual({ ok: true, port: 3001 }); + }); + + test("prefix typos are refused", () => { + expect(listenPort("30o10", 3010).ok).toBe(false); + expect(listenPort("3001abc", 3001).ok).toBe(false); + }); +}); diff --git a/app/vite.config.ts b/app/vite.config.ts index e17104ea0..1379c8f2d 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -3,6 +3,7 @@ import tailwindcss from "@tailwindcss/vite"; import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +import { listenPort } from "../shared/listen-port"; /* * The same address and the same proxy whether this is the dev server or the preview of a build. @@ -11,19 +12,29 @@ import { defineConfig } from "vite"; * app rather than the dev server gets no `/api` proxy unless it is repeated here. A desktop install * serves the build, and without this every call it makes returns the app's own HTML. */ +const appPort = listenPort(process.env.APP_PORT, 3010); +if (!appPort.ok) { + throw new Error(appPort.reason.replace(/^PORT /, "APP_PORT ")); +} +const apiPort = listenPort(process.env.SERVER_PORT, 3001); +if (!apiPort.ok) { + throw new Error(apiPort.reason.replace(/^PORT /, "SERVER_PORT ")); +} + const serving = { // Both loopbacks, which is what `::` gets you: Node opens a dual-stack socket, so 127.0.0.1 and // ::1 both answer. Left to itself Vite binds whichever one this runtime resolves `localhost` // to, which is ::1 under Node and 127.0.0.1 under bun, and the other address is then refused. // Whoever is told the URL has no way to know which they were given. + // Empty APP_PORT=/SERVER_PORT= is unset (compose / leftover .env), not NaN — same trap as Bot PORT. host: "::", - port: Number.parseInt(process.env.APP_PORT ?? "3010", 10), + port: appPort.port, strictPort: true, proxy: { // `ws: true` is required for the live screen. Without it Vite answers the upgrade request with // the app's HTML and the socket fails with an opaque error that looks like a server problem. "/api": { - target: `http://localhost:${process.env.SERVER_PORT ?? "3001"}`, + target: `http://localhost:${apiPort.port}`, ws: true, }, },