Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
24 changes: 24 additions & 0 deletions app/vite-listen-port.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
15 changes: 13 additions & 2 deletions app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
},
},
Expand Down