fix(tlon): add timeout to SSE client fetch calls (CWE-400) (#5926)

Add timeout protection to prevent indefinite hangs when Urbit server
becomes unresponsive or network partition occurs.

Changes:
- Add AbortSignal.timeout(30_000) to 7 one-shot fetch calls
- Add AbortController with 60s connection timeout to SSE stream fetch
  (clears timeout after headers received to avoid aborting active stream)

Affected methods: sendSubscription, connect, openStream, poke, scry, close

Fixes #5266

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hcl
2026-02-02 07:40:27 +08:00
committed by GitHub
parent 19775abdda
commit 411d5fda58

View File

@@ -114,6 +114,7 @@ export class UrbitSSEClient {
Cookie: this.cookie,
},
body: JSON.stringify([subscription]),
signal: AbortSignal.timeout(30_000),
});
if (!response.ok && response.status !== 204) {
@@ -130,6 +131,7 @@ export class UrbitSSEClient {
Cookie: this.cookie,
},
body: JSON.stringify(this.subscriptions),
signal: AbortSignal.timeout(30_000),
});
if (!createResp.ok && createResp.status !== 204) {
@@ -152,6 +154,7 @@ export class UrbitSSEClient {
json: "Opening API channel",
},
]),
signal: AbortSignal.timeout(30_000),
});
if (!pokeResp.ok && pokeResp.status !== 204) {
@@ -164,14 +167,23 @@ export class UrbitSSEClient {
}
async openStream() {
// Use AbortController with manual timeout so we only abort during initial connection,
// not after the SSE stream is established and actively streaming.
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 60_000);
const response = await fetch(this.channelUrl, {
method: "GET",
headers: {
Accept: "text/event-stream",
Cookie: this.cookie,
},
signal: controller.signal,
});
// Clear timeout once connection established (headers received)
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Stream connection failed: ${response.status}`);
}
@@ -279,6 +291,7 @@ export class UrbitSSEClient {
Cookie: this.cookie,
},
body: JSON.stringify([pokeData]),
signal: AbortSignal.timeout(30_000),
});
if (!response.ok && response.status !== 204) {
@@ -296,6 +309,7 @@ export class UrbitSSEClient {
headers: {
Cookie: this.cookie,
},
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
@@ -364,6 +378,7 @@ export class UrbitSSEClient {
Cookie: this.cookie,
},
body: JSON.stringify(unsubscribes),
signal: AbortSignal.timeout(30_000),
});
await fetch(this.channelUrl, {
@@ -371,6 +386,7 @@ export class UrbitSSEClient {
headers: {
Cookie: this.cookie,
},
signal: AbortSignal.timeout(30_000),
});
} catch (error) {
this.logger.error?.(`Error closing channel: ${String(error)}`);