Skip to content
Merged
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
77 changes: 43 additions & 34 deletions packages/host-service/src/tunnel/tunnel-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,43 +41,52 @@ export class TunnelClient {
async connect(): Promise<void> {
if (this.closed) return;

const token = await this.getAuthToken();
if (!token) {
console.warn("[host-service:tunnel] no auth token available, retrying");
this.scheduleReconnect();
return;
}

const url = new URL("/tunnel", this.relayUrl);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
url.searchParams.set("hostId", this.hostId);
url.searchParams.set("token", token);

const socket = new WebSocket(url.toString());
this.socket = socket;

socket.onopen = () => {
this.reconnectAttempts = 0;
console.log(
`[host-service:tunnel] connected to relay for host ${this.hostId}`,
);
};

socket.onmessage = (event) => {
void this.handleMessage(event.data);
};

socket.onclose = () => {
this.socket = null;
this.cleanupChannels();
if (!this.closed) {
// An unhandled rejection here (e.g. DNS failure inside getAuthToken on
// wake from sleep) crashes host-service and orphans every PTY.
try {
const token = await this.getAuthToken();
if (!token) {
console.warn("[host-service:tunnel] no auth token available, retrying");
this.scheduleReconnect();
return;
}
};

socket.onerror = (event) => {
console.error("[host-service:tunnel] socket error:", event);
};
const url = new URL("/tunnel", this.relayUrl);
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
url.searchParams.set("hostId", this.hostId);
url.searchParams.set("token", token);

const socket = new WebSocket(url.toString());
this.socket = socket;
Comment on lines +46 to +60
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Re-check this.closed after the awaited token fetch.

close() can run while getAuthToken() is in flight. In that case this method still creates a new relay socket after shutdown, because the only closed guard is before Line 51. That leaves a live connection behind a client that was already closed.

Suggested fix
 		try {
 			const token = await this.getAuthToken();
+			if (this.closed) {
+				return;
+			}
+
 			if (!token) {
 				console.warn("[host-service:tunnel] no auth token available, retrying");
 				this.scheduleReconnect();
 				return;
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/host-service/src/tunnel/tunnel-client.ts` around lines 50 - 64,
After awaiting getAuthToken(), re-check the instance closed flag so you don't
open a socket after close() runs: inside the same method (the block that calls
getAuthToken()), add an immediate guard like "if (this.closed) return;" after
the await (before constructing the URL / new WebSocket) and ensure you do not
call scheduleReconnect or create this.socket when closed; this prevents creating
a live WebSocket on a client that was shut down (references: getAuthToken,
close, scheduleReconnect, this.closed, this.socket).


socket.onopen = () => {
this.reconnectAttempts = 0;
console.log(
`[host-service:tunnel] connected to relay for host ${this.hostId}`,
);
};

socket.onmessage = (event) => {
void this.handleMessage(event.data);
};

socket.onclose = () => {
this.socket = null;
this.cleanupChannels();
if (!this.closed) {
this.scheduleReconnect();
}
};

socket.onerror = (event) => {
console.error("[host-service:tunnel] socket error:", event);
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[host-service:tunnel] connect failed: ${message}`);
this.socket = null;
this.scheduleReconnect();
Comment on lines +84 to +88
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Log the full error for better diagnostics

Only error.message is logged here, which strips the [cause] chain that carries the root cause (e.g. getaddrinfo ENOTFOUND api.superset.sh). Passing the original error object as a second argument to console.error preserves the full stack and cause chain in the log file.

Suggested change
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[host-service:tunnel] connect failed: ${message}`);
this.socket = null;
this.scheduleReconnect();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`[host-service:tunnel] connect failed: ${message}`, error);
this.socket = null;
this.scheduleReconnect();
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/host-service/src/tunnel/tunnel-client.ts
Line: 88-92

Comment:
**Log the full error for better diagnostics**

Only `error.message` is logged here, which strips the `[cause]` chain that carries the root cause (e.g. `getaddrinfo ENOTFOUND api.superset.sh`). Passing the original error object as a second argument to `console.error` preserves the full stack and cause chain in the log file.

```suggestion
} catch (error) {
			const message = error instanceof Error ? error.message : String(error);
			console.error(`[host-service:tunnel] connect failed: ${message}`, error);
			this.socket = null;
			this.scheduleReconnect();
		}
```

How can I resolve this? If you propose a fix, please make it concise.

}
}

close(): void {
Expand Down
Loading