diff --git a/CHANGELOG.md b/CHANGELOG.md index 02adfca6b..b10063021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ from published versions since it shows up in the VS Code extension changelog tab and is confusing to users. Add it back between releases if needed. --> +## Unreleased + +### Fixed + +- Apply a 60-second default timeout to REST requests, so requests hung on a + half-open TCP connection don't stall pollers forever. + ## [v1.15.2](https://github.com/coder/vscode-coder/releases/tag/v1.15.2) 2026-06-30 > **Breaking:** API requests now respect `http.proxySupport: off`. Previously diff --git a/src/api/coderApi.ts b/src/api/coderApi.ts index 8f8ef8a6f..8eb140292 100644 --- a/src/api/coderApi.ts +++ b/src/api/coderApi.ts @@ -70,6 +70,14 @@ import type { const coderSessionTokenHeader = "Coder-Session-Token"; +/** + * Default timeout for REST requests, so requests hung on half-open TCP + * connections (e.g. after system sleep) don't stall pollers forever. + * Streaming responses are only bounded until response headers arrive; + * axios never aborts an in-flight stream body. + */ +export const DEFAULT_REQUEST_TIMEOUT_MS = 60_000; + /** * Configuration settings that affect WebSocket connections. * Changes to these settings will trigger WebSocket reconnection. @@ -130,6 +138,7 @@ export class CoderApi extends Api implements vscode.Disposable { httpRequestsTelemetry, authConfigTracker, ); + client.getAxiosInstance().defaults.timeout = DEFAULT_REQUEST_TIMEOUT_MS; client.setCredentials(baseUrl, token); setupInterceptors(client, output, httpRequestsTelemetry, authConfigTracker); diff --git a/test/unit/api/coderApi.test.ts b/test/unit/api/coderApi.test.ts index 78f43044b..16c2847dd 100644 --- a/test/unit/api/coderApi.test.ts +++ b/test/unit/api/coderApi.test.ts @@ -21,7 +21,7 @@ import { getRefreshCommand, refreshCertificates, } from "@/api/certificateRefresh"; -import { CoderApi } from "@/api/coderApi"; +import { CoderApi, DEFAULT_REQUEST_TIMEOUT_MS } from "@/api/coderApi"; import { createHttpAgent } from "@/api/utils"; import { CONFIG_CHANGE_DEBOUNCE_MS } from "@/configWatcher"; import { ClientCertificateError } from "@/error/clientCertificateError"; @@ -131,6 +131,30 @@ describe("CoderApi", () => { }); describe("HTTP Interceptors", () => { + it("sets a default request timeout on the axios instance", () => { + api = createApi(); + + expect(api.getAxiosInstance().defaults.timeout).toBe( + DEFAULT_REQUEST_TIMEOUT_MS, + ); + }); + + it("applies the default timeout to requests", async () => { + api = createApi(); + const response = await api.getAxiosInstance().get("/api/v2/users/me"); + + expect(response.config.timeout).toBe(DEFAULT_REQUEST_TIMEOUT_MS); + }); + + it("allows per-request timeout overrides", async () => { + api = createApi(); + const response = await api + .getAxiosInstance() + .get("/api/v2/users/me", { timeout: 0 }); + + expect(response.config.timeout).toBe(0); + }); + it("adds custom headers and HTTP agent to requests", async () => { const mockAgent = new ProxyAgent(); vi.mocked(createHttpAgent).mockResolvedValue(mockAgent);