fix(network): apply connection timeout in server and extension hosts

Complete the runtime entrypoints from #3404 without changing address-family selection.
This commit is contained in:
Bohdan Triapitsyn
2026-09-07 20:37:46 +03:00
parent 5ae1a949c8
commit 45bc61f8f9
7 changed files with 74 additions and 0 deletions
+8
View File
@@ -190,6 +190,14 @@ Reachable filesystem routes: `api:fs:read` (attachments), `api:fs:search`
Maintenance: reviews, changelog entries, and parity claims consult this map; Maintenance: reviews, changelog entries, and parity claims consult this map;
whoever mounts or unmounts a surface updates it in the same change. whoever mounts or unmounts a surface updates it in the same change.
## Network connections
Extension activation applies `networkDefaults.ts` before registering handlers.
It gives Node connection attempts 5 seconds, matching the web runtime, so quota
requests to distant providers can connect. This is an extension-host process
default, including other Node connections in that host. Address-family selection
stays unchanged; runtimes without the setter retain their existing behavior.
## Global OpenCode paths ## Global OpenCode paths
`opencodeConfigPaths.ts` owns the global config directory for config CRUD, `opencodeConfigPaths.ts` owns the global config directory for config CRUD,
+2
View File
@@ -7,6 +7,7 @@ import { startGlobalEventWatcher, stopGlobalEventWatcher, setChatViewProvider }
import { pathsEqualWithNormalizedDriveLetter } from './pathUtils'; import { pathsEqualWithNormalizedDriveLetter } from './pathUtils';
import { resolveWorkspaceFolders } from './workspaceResolver'; import { resolveWorkspaceFolders } from './workspaceResolver';
import { InlineCommentThreads, SIDEBAR_SURFACE_ID } from './InlineCommentThreads'; import { InlineCommentThreads, SIDEBAR_SURFACE_ID } from './InlineCommentThreads';
import { applyConnectAttemptTimeout } from './networkDefaults';
let chatViewProvider: ChatViewProvider | undefined; let chatViewProvider: ChatViewProvider | undefined;
@@ -52,6 +53,7 @@ const formatDurationMs = (value: number | null | undefined) => {
}; };
export async function activate(context: vscode.ExtensionContext) { export async function activate(context: vscode.ExtensionContext) {
applyConnectAttemptTimeout();
outputChannel = vscode.window.createOutputChannel('OpenChamber'); outputChannel = vscode.window.createOutputChannel('OpenChamber');
let moveToRightSidebarScheduled = false; let moveToRightSidebarScheduled = false;
@@ -0,0 +1,24 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as net from 'node:net';
import { applyConnectAttemptTimeout } from './networkDefaults';
test('allows slow connections without changing address-family selection', () => {
const previousTimeout = net.getDefaultAutoSelectFamilyAttemptTimeout();
const previousFamily = net.getDefaultAutoSelectFamily();
try {
net.setDefaultAutoSelectFamilyAttemptTimeout(250);
assert.equal(applyConnectAttemptTimeout(), true);
assert.equal(net.getDefaultAutoSelectFamilyAttemptTimeout(), 5_000);
assert.equal(net.getDefaultAutoSelectFamily(), previousFamily);
} finally {
net.setDefaultAutoSelectFamilyAttemptTimeout(previousTimeout);
}
});
test('unsupported runtimes retain their existing behavior', () => {
assert.equal(applyConnectAttemptTimeout({}), false);
assert.equal(applyConnectAttemptTimeout({
setDefaultAutoSelectFamilyAttemptTimeout() { throw new Error('unsupported'); },
}), false);
});
+15
View File
@@ -0,0 +1,15 @@
import * as net from 'node:net';
// Mirrors the web runtime policy for distant quota endpoints. The extension
// host has its own Node fetch stack and does not inherit server defaults.
export function applyConnectAttemptTimeout(
netModule: Partial<Pick<typeof net, 'setDefaultAutoSelectFamilyAttemptTimeout'>> = net,
): boolean {
try {
if (!netModule.setDefaultAutoSelectFamilyAttemptTimeout) return false;
netModule.setDefaultAutoSelectFamilyAttemptTimeout(5_000);
return true;
} catch {
return false;
}
}
+4
View File
@@ -118,6 +118,10 @@ import { createScheduledTaskService } from './lib/scheduled-tasks/service.js';
import { createOpenChamberControlService } from './lib/openchamber-control/service.js'; import { createOpenChamberControlService } from './lib/openchamber-control/service.js';
import { OpenChamberControlError } from './lib/openchamber-control/error.js'; import { OpenChamberControlError } from './lib/openchamber-control/error.js';
import webPush from 'web-push'; import webPush from 'web-push';
import { applyConnectAttemptTimeout } from './lib/network-defaults.js';
// Background CLI launches enter here in a fresh process, without CLI defaults.
applyConnectAttemptTimeout();
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -1,9 +1,25 @@
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import net from 'node:net'; import net from 'node:net';
import { spawnSync } from 'node:child_process';
import { applyConnectAttemptTimeout, CONNECT_ATTEMPT_TIMEOUT_MS } from './network-defaults.js'; import { applyConnectAttemptTimeout, CONNECT_ATTEMPT_TIMEOUT_MS } from './network-defaults.js';
describe('applyConnectAttemptTimeout', () => { describe('applyConnectAttemptTimeout', () => {
it('initializes the server entrypoint in a fresh Node process', () => {
const serverUrl = new URL('../index.js', import.meta.url).href;
const result = spawnSync('node', ['--input-type=module', '--eval', `
import net from 'node:net';
import assert from 'node:assert/strict';
const family = net.getDefaultAutoSelectFamily();
net.setDefaultAutoSelectFamilyAttemptTimeout(250);
await import(${JSON.stringify(serverUrl)});
assert.equal(net.getDefaultAutoSelectFamilyAttemptTimeout(), 5000);
assert.equal(net.getDefaultAutoSelectFamily(), family);
process.exit(0);
`], { encoding: 'utf8', timeout: 15_000, windowsHide: true });
expect(result.status, result.stderr).toBe(0);
});
it('raises the per-attempt connect timeout on runtimes that expose the setter', () => { it('raises the per-attempt connect timeout on runtimes that expose the setter', () => {
const previous = net.getDefaultAutoSelectFamilyAttemptTimeout(); const previous = net.getDefaultAutoSelectFamilyAttemptTimeout();
try { try {
@@ -3,6 +3,11 @@
## Purpose ## Purpose
This module fetches quota and usage signals for supported providers in the web server runtime. This module fetches quota and usage signals for supported providers in the web server runtime.
Node server entrypoints apply `../network-defaults.js` before serving requests,
including the daemon launched directly through `server/index.js`. Connection
attempts get 5 seconds without changing address-family selection. The VS Code
extension applies the same policy in its own process at activation.
## Entrypoints and structure ## Entrypoints and structure
- `packages/web/server/lib/quota/index.js`: public entrypoint imported by `packages/web/server/index.js`. - `packages/web/server/lib/quota/index.js`: public entrypoint imported by `packages/web/server/index.js`.
- `packages/web/server/lib/quota/routes.js`: Express route registration for quota endpoints. - `packages/web/server/lib/quota/routes.js`: Express route registration for quota endpoints.