Skip to content

Commit adefab2

Browse files
committed
Add 30s request timeout to CoderClient.request()
Without a timeout, a hanging server (accepts TCP, never responds) blocks every API call indefinitely. The GitHub Action then burns CI minutes until the job-level timeout kills it. Uses AbortSignal.timeout(30_000) as default, respects caller signal.
1 parent e49abc1 commit adefab2

3 files changed

Lines changed: 65 additions & 8 deletions

File tree

dist/index.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,43 @@ var __getProtoOf = Object.getPrototypeOf;
33
var __defProp = Object.defineProperty;
44
var __getOwnPropNames = Object.getOwnPropertyNames;
55
var __hasOwnProp = Object.prototype.hasOwnProperty;
6+
function __accessProp(key) {
7+
return this[key];
8+
}
9+
var __toESMCache_node;
10+
var __toESMCache_esm;
611
var __toESM = (mod, isNodeMode, target) => {
12+
var canCache = mod != null && typeof mod === "object";
13+
if (canCache) {
14+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
15+
var cached = cache.get(mod);
16+
if (cached)
17+
return cached;
18+
}
719
target = mod != null ? __create(__getProtoOf(mod)) : {};
820
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
921
for (let key of __getOwnPropNames(mod))
1022
if (!__hasOwnProp.call(to, key))
1123
__defProp(to, key, {
12-
get: () => mod[key],
24+
get: __accessProp.bind(mod, key),
1325
enumerable: true
1426
});
27+
if (canCache)
28+
cache.set(mod, to);
1529
return to;
1630
};
1731
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
32+
var __returnValue = (v) => v;
33+
function __exportSetter(name, newValue) {
34+
this[name] = __returnValue.bind(null, newValue);
35+
}
1836
var __export = (target, all) => {
1937
for (var name in all)
2038
__defProp(target, name, {
2139
get: all[name],
2240
enumerable: true,
2341
configurable: true,
24-
set: (newValue) => all[name] = () => newValue
42+
set: __exportSetter.bind(all, name)
2543
});
2644
};
2745

@@ -3447,7 +3465,7 @@ var require_constants2 = __commonJS((exports2, module2) => {
34473465
}
34483466
})();
34493467
var channel;
3450-
var structuredClone = globalThis.structuredClone ?? function structuredClone(value, options = undefined) {
3468+
var structuredClone = globalThis.structuredClone ?? function structuredClone2(value, options = undefined) {
34513469
if (arguments.length === 0) {
34523470
throw new TypeError("missing argument");
34533471
}
@@ -16372,7 +16390,7 @@ var require_undici = __commonJS((exports2, module2) => {
1637216390
module2.exports.getGlobalDispatcher = getGlobalDispatcher;
1637316391
if (util.nodeMajor > 16 || util.nodeMajor === 16 && util.nodeMinor >= 8) {
1637416392
let fetchImpl = null;
16375-
module2.exports.fetch = async function fetch(resource) {
16393+
module2.exports.fetch = async function fetch2(resource) {
1637616394
if (!fetchImpl) {
1637716395
fetchImpl = require_fetch().fetch;
1637816396
}
@@ -22708,11 +22726,11 @@ var require_github = __commonJS((exports2) => {
2270822726
});
2270922727

2271022728
// src/index.ts
22711-
var core2 = __toESM(require_core());
22712-
var github = __toESM(require_github());
22729+
var core2 = __toESM(require_core(), 1);
22730+
var github = __toESM(require_github(), 1);
2271322731

2271422732
// src/action.ts
22715-
var core = __toESM(require_core());
22733+
var core = __toESM(require_core(), 1);
2271622734

2271722735
class CoderAgentChatAction {
2271822736
coder;
@@ -26818,7 +26836,8 @@ class RealCoderClient {
2681826836
const url = `${this.serverURL}${endpoint}`;
2681926837
const response = await fetch(url, {
2682026838
...options,
26821-
headers: { ...this.headers, ...options?.headers }
26839+
headers: { ...this.headers, ...options?.headers },
26840+
signal: options?.signal ?? AbortSignal.timeout(30000)
2682226841
});
2682326842
if (!response.ok) {
2682426843
const body = await response.text().catch(() => "");

src/coder-client.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,41 @@ describe("CoderClient", () => {
156156
);
157157
});
158158
});
159+
160+
describe("request timeout", () => {
161+
test("aborts when server never responds", async () => {
162+
const original = AbortSignal.timeout;
163+
try {
164+
// Use a 10ms timeout so the test doesn't wait 30s.
165+
AbortSignal.timeout = () => original.call(AbortSignal, 10);
166+
mockFetch.mockImplementation(
167+
(_url: string, init?: RequestInit) =>
168+
new Promise((_resolve, reject) => {
169+
// Keep event loop alive so the abort timer fires.
170+
const keepalive = setTimeout(() => {}, 5000);
171+
if (init?.signal?.aborted) {
172+
clearTimeout(keepalive);
173+
reject(init.signal.reason);
174+
return;
175+
}
176+
init?.signal?.addEventListener("abort", () => {
177+
clearTimeout(keepalive);
178+
reject(init.signal?.reason);
179+
});
180+
}),
181+
);
182+
await expect(client.getChat(mockChat.id)).rejects.toThrow();
183+
} finally {
184+
AbortSignal.timeout = original;
185+
}
186+
});
187+
188+
test("passes signal to fetch", async () => {
189+
mockFetch.mockResolvedValue(createMockResponse(mockChat));
190+
await client.getChat(mockChat.id);
191+
const call = mockFetch.mock.calls[0];
192+
const init = call[1] as RequestInit;
193+
expect(init.signal).toBeInstanceOf(AbortSignal);
194+
});
195+
});
159196
});

src/coder-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class RealCoderClient implements CoderClient {
3838
const response = await fetch(url, {
3939
...options,
4040
headers: { ...this.headers, ...options?.headers },
41+
signal: options?.signal ?? AbortSignal.timeout(30_000),
4142
});
4243

4344
if (!response.ok) {

0 commit comments

Comments
 (0)