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:
parent
19775abdda
commit
411d5fda58
1 changed files with 16 additions and 0 deletions
|
|
@ -114,6 +114,7 @@ export class UrbitSSEClient {
|
||||||
Cookie: this.cookie,
|
Cookie: this.cookie,
|
||||||
},
|
},
|
||||||
body: JSON.stringify([subscription]),
|
body: JSON.stringify([subscription]),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && response.status !== 204) {
|
if (!response.ok && response.status !== 204) {
|
||||||
|
|
@ -130,6 +131,7 @@ export class UrbitSSEClient {
|
||||||
Cookie: this.cookie,
|
Cookie: this.cookie,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(this.subscriptions),
|
body: JSON.stringify(this.subscriptions),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createResp.ok && createResp.status !== 204) {
|
if (!createResp.ok && createResp.status !== 204) {
|
||||||
|
|
@ -152,6 +154,7 @@ export class UrbitSSEClient {
|
||||||
json: "Opening API channel",
|
json: "Opening API channel",
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!pokeResp.ok && pokeResp.status !== 204) {
|
if (!pokeResp.ok && pokeResp.status !== 204) {
|
||||||
|
|
@ -164,14 +167,23 @@ export class UrbitSSEClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async openStream() {
|
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, {
|
const response = await fetch(this.channelUrl, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "text/event-stream",
|
Accept: "text/event-stream",
|
||||||
Cookie: this.cookie,
|
Cookie: this.cookie,
|
||||||
},
|
},
|
||||||
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear timeout once connection established (headers received)
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Stream connection failed: ${response.status}`);
|
throw new Error(`Stream connection failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -279,6 +291,7 @@ export class UrbitSSEClient {
|
||||||
Cookie: this.cookie,
|
Cookie: this.cookie,
|
||||||
},
|
},
|
||||||
body: JSON.stringify([pokeData]),
|
body: JSON.stringify([pokeData]),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && response.status !== 204) {
|
if (!response.ok && response.status !== 204) {
|
||||||
|
|
@ -296,6 +309,7 @@ export class UrbitSSEClient {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: this.cookie,
|
Cookie: this.cookie,
|
||||||
},
|
},
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -364,6 +378,7 @@ export class UrbitSSEClient {
|
||||||
Cookie: this.cookie,
|
Cookie: this.cookie,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(unsubscribes),
|
body: JSON.stringify(unsubscribes),
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
await fetch(this.channelUrl, {
|
await fetch(this.channelUrl, {
|
||||||
|
|
@ -371,6 +386,7 @@ export class UrbitSSEClient {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: this.cookie,
|
Cookie: this.cookie,
|
||||||
},
|
},
|
||||||
|
signal: AbortSignal.timeout(30_000),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error?.(`Error closing channel: ${String(error)}`);
|
this.logger.error?.(`Error closing channel: ${String(error)}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue