Dashboard › sentry › Session 0nrqYGUubOz0
0nrqYGUubOz0JEXIHInvestigate Sentry issue LOREAI-GATEWAY-1Z: TypeError: Failed to parse URL from http://127.0.0.1:undefined/health
Context from Sentry:
/tmp/test-startserver.mjs line 13startServer() returned an object where .port and .hosts are both undefinedconst server = startServer(config); server.port; await server.ready; — the port is undefined even AFTER await server.ready resolvesThe test script:
const server = startServer(config);
console.log("port:", server.port, "hosts:", server.hosts);
await server.ready;
console.log("ready!");
const r = await fetch(`http://127.0.0.1:${server.port}/health`);
Your task:
Read packages/gateway/src/server.ts — find:
startServer() function.port is set on the returned object.ready promise and what it resolves to.port can be undefined if the server hasn't started listening yetCheck if startServer() uses server.address() to get the port — in Node.js, this returns null until the server is actively listening
Check if there's a race condition where ready resolves but port hasn't been set yet
Look at the config object — does it require a port to be specified, or does it default to 0 (random)?
Check if the port property is set synchronously vs asynchronously
Return: (1) exact code path where port becomes undefined with file:line references, (2) root cause analysis, (3) proposed fix to ensure port is always defined after ready resolves
I'll investigate this systematically. Let me start by reading the server file and related configuration.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts</path> <type>file</type> <content> 1: /** 2: * HTTP server for the Lore gateway proxy. 3: * 4: * Routes: 5: * POST /v1/messages → Anthropic protocol 6: * POST /v1/chat/completions → OpenAI Chat Completions protocol 7: * POST /v1/responses → OpenAI Responses API protocol 8: * POST /v1/responses/compact → Codex compaction (Responses API) 9: * POST /v1/compact → Explicit compaction summary (Pi plugin, etc.) 10: * GET /v1/models → Passthrough to upstream 11: * GET /health → Health check 12: * 13: * Uses `node:http` `createServer` with Web `Request`/`Response` — the same 14: * code runs under both Bun and the Node.js npm distribution. ...
Found 9 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts:
Line 289: export async function startServer(config: GatewayConfig): Promise<{
Line 297: // loadConfig() always provides these, but startServer is a public export.
Line 524: * Used by startServer() to surface the async bind to callers.
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/index.ts:
Line 7: * import { startServer, loadConfig } from "@loreai/gateway";
Line 22: export { startServer } from "./server";
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/cli/start.ts:
Line 7: import { startServer } from "../server";
Line 112: // startServer() is async — it binds each host sequentially so the
Line 114: // returned `ready` promise is the same one startServer already
...
Found 4 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts:
Line 306: if (!Number.isFinite(config.port) || config.port < 0) {
Line 434: // Bind sequentially so the OS-assigned port (when config.port is 0)
Line 439: let resolvedPort = config.port;
Line 496: resolvedPort = addr.port;
Now I see the key issue. startServer() is async — it returns a Promise. Let me check how the test script and the CLI use it, and verify the config and DEFAULT_PORT.
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/config.ts</path>
<type>file</type>
<content>
1: /**
2: * Gateway configuration — loaded from environment variables with sensible
3: * defaults. No Zod, no file-based config — minimal @loreai/core dependency
4: * (only `normalizeRemoteUrl` for git URL canonicalization).
5: */
6:
7: import {
8: normalizeRemoteUrl,
9: discoverWorkspaceRoot,
10: UNATTRIBUTED_PROJECT_PREFIX,
11: isUnattributedProjectPath,
12: } from "@loreai/core";
13:
14: // ---------------------------------------------------------------------------
15: // Port defaults
16: // ---------------------------------------------------------------------------
17:
18: /**
19: * Default port preference order when LORE_LISTEN_PORT is not set. ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/cli/start.ts</path>
<type>file</type>
<content>
1: /**
2: * `lore start` — start the gateway server without auto-launching an agent.
3: *
4: * Extracted from the old top-level index.ts boot logic.
5: */
6: import { loadConfig, DEFAULT_PORTS, type GatewayConfig } from "../config";
7: import { startServer } from "../server";
8: import { resetPipelineState } from "../pipeline";
9: import { writePortFile, removePortFile } from "../portfile";
10: import { embedding } from "@loreai/core";
11: import { safeExit } from "./exit";
12:
13: export interface StartOptions {
14: port?: number;
15: hosts?: string[];
16: debug?: boolean;
17: /** Suppress verbose banner (env vars, export hints). Used in embedded mode. */
18: quiet?: boolean;
19: /** Remote gateway URL. When set, `lore run` delegates to this gateway
20: * instead of starting a local one. ...
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/index.ts</path>
<type>file</type>
<content>
1: /**
2: * Lore Gateway — package entry point.
3: *
4: * Library exports for programmatic use, plus `_cli()` for the CLI binary.
5: *
6: * Library usage:
7: * import { startServer, loadConfig } from "@loreai/gateway";
8: *
9: * CLI usage (via bin wrapper):
10: * lore start
11: * lore run claude
12: */
13: import "../instrument";
14: import { fileURLToPath } from "node:url";
15:
16: // ---------------------------------------------------------------------------
17: // Library API
18: // ---------------------------------------------------------------------------
19:
20: export { loadConfig, DEFAULT_PORTS, DEFAULT_PORT } from "./config";
21: export type { GatewayConfig } from "./config";
22: export { startServer } from "./server";
23: export { handleRequest, resetPipelineState } from "./pipeline";
24: ...
Now I have the complete picture. Let me verify my understanding by checking the exact return type and the startGateway usage pattern one more time.
Found 10 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/recall-openai-stream.test.ts:
Line 137: const server = await startServer(config);
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/helpers/harness.ts:
Line 83: const server = await startServer(config);
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/api.test.ts:
Line 41: server = await startServer(config);
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/script/bundle.ts:
Line 342: export declare function startServer(config: GatewayConfig): Promise<{
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/src/server.ts:
Line 289: export async function ...
Found 9 matches
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/recall-openai-stream.test.ts:
Line 118: const { startServer } = await import("../src/server");
Line 137: const server = await startServer(config);
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/hosted-config.test.ts:
Line 2: * Tests for hosted mode configuration defaults in startGateway().
Line 4: * The override logic in startGateway() follows three-tier precedence:
Line 15: * Replicate the hosted mode override logic from startGateway() so we can
Line 73: // startGateway override logic
Line 76: describe("startGateway override logic", () => {
/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/test/api.test.ts:
Line 29: const { startServer } = await import("../src/server");
...
Now let me check the bundle.ts declaration to see what the published npm package exposes — this is what the test script at /tmp/test-startserver.mjs would import:
<path>/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/gateway/script/bundle.ts</path>
<type>file</type>
<content>
330: /** The primary default port (first in the fallback chain). */
331: export declare const DEFAULT_PORT: 3207;
332:
333: /** Load Lore gateway configuration from the environment. */
334: export declare function loadConfig(): GatewayConfig;
335:
336: /**
337: * Start the Lore gateway server.
338: *
339: * Prefer startGateway() for most use cases — it handles port fallback,
340: * port file management, and existing-instance reuse automatically.
341: */
342: export declare function startServer(config: GatewayConfig): Promise<{
343: stop: () => void;
344: port: number;
345: hosts: string[];
346: /** Resolves when all bound servers are listening. ...
I now have the complete picture. Here is my analysis:
The bug is not in startServer() itself — the function is correct internally. The bug is a caller-side misuse caused by the function's async signature being easy to miss.
startServer() signature — packages/gateway/src/server.ts:289:
export async function startServer(config: GatewayConfig): Promise<{
stop: () => void;
port: number;
hosts: string[];
ready: Promise<void>;
}>
startServer() is an async function that returns a Promise. It does NOT return the server object synchronously.
The failing test script does:
const server = startServer(config); // <-- NO await!
console.log("port:", server.port, "hosts:", server.hosts); // Promise has no .port → undefined
await server.ready; // Promise has no .ready → undefined, await undefined resolves immediately
const r = await fetch(`http://127.0.0.1:${server.port}/health`); // port is still undefined → boom
Here is what happens step by step:
Line const server = startServer(config) — Without await, server is a Promise object, not the resolved { stop, port, hosts, ready } object.
Line console.log("port:", server.port, "hosts:", server.hosts) — Promise objects have no .port or .hosts properties, so both are undefined. This matches the Sentry breadcrumb exactly: "port: undefined hosts: undefined".
Line await server.ready — server.ready is undefined (because server is a Promise, not the server object). await undefined resolves immediately as a no-op, so the script proceeds without waiting for the server to actually start.
**Line fetch(\http://127.0.0.1:${server.port}/health`)** — server.portis stillundefined, producing the URL http://127.0.0.1:undefined/health, which causes TypeError: Failed to parse URL from ...