Merge pull request #187 from quocthai0404/fix/issue-176-windows-callback-port

fix: Make OAuth callback port configurable for Windows compatibility (#176)
This commit is contained in:
Badri Narayanan S
2026-01-26 13:45:13 +05:30
committed by GitHub
4 changed files with 156 additions and 15 deletions

View File

@@ -435,6 +435,64 @@ npm run test:cache-control # Cache control field stripping
## Troubleshooting ## Troubleshooting
### Windows: OAuth Port Error (EACCES)
On Windows, the default OAuth callback port (51121) may be reserved by Hyper-V, WSL2, or Docker. If you see:
```
Error: listen EACCES: permission denied 0.0.0.0:51121
```
The proxy will automatically try fallback ports (51122-51126). If all ports fail, try these solutions:
#### Option 1: Use a Custom Port (Recommended)
Set a custom port outside the reserved range:
```bash
# Windows PowerShell
$env:OAUTH_CALLBACK_PORT = "3456"
antigravity-claude-proxy start
# Windows CMD
set OAUTH_CALLBACK_PORT=3456
antigravity-claude-proxy start
# Or add to your .env file
OAUTH_CALLBACK_PORT=3456
```
#### Option 2: Reset Windows NAT
Run as Administrator:
```powershell
net stop winnat
net start winnat
```
#### Option 3: Check Reserved Ports
See which ports are reserved:
```powershell
netsh interface ipv4 show excludedportrange protocol=tcp
```
If 51121 is in a reserved range, use Option 1 with a port outside those ranges.
#### Option 4: Permanently Exclude Port (Admin)
Reserve the port before Hyper-V claims it (run as Administrator):
```powershell
netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
```
> **Note:** The server automatically tries fallback ports (51122-51126) if the primary port fails.
---
### "Could not extract token from Antigravity" ### "Could not extract token from Antigravity"
If using single-account mode with Antigravity: If using single-account mode with Antigravity:

View File

@@ -36,6 +36,9 @@
"maxAccounts": 10, "maxAccounts": 10,
"_maxAccounts_comment": "Maximum number of Google accounts allowed (1-100). Default: 10.", "_maxAccounts_comment": "Maximum number of Google accounts allowed (1-100). Default: 10.",
"_oauthCallbackPort_comment": "OAuth callback server port. Change if you get EACCES errors on Windows. Can also use OAUTH_CALLBACK_PORT env var. Default: 51121.",
"_oauthCallbackPort_env": "OAUTH_CALLBACK_PORT=3456",
"_profiles": { "_profiles": {
"development": { "development": {
"debug": true, "debug": true,

View File

@@ -137,22 +137,50 @@ export function extractCodeFromInput(input) {
return { code: trimmed, state: null }; return { code: trimmed, state: null };
} }
/**
* Attempt to bind server to a specific port
* @param {http.Server} server - HTTP server instance
* @param {number} port - Port to bind to
* @returns {Promise<number>} Resolves with port on success, rejects on error
*/
function tryBindPort(server, port) {
return new Promise((resolve, reject) => {
const onError = (err) => {
server.removeListener('listening', onSuccess);
reject(err);
};
const onSuccess = () => {
server.removeListener('error', onError);
resolve(port);
};
server.once('error', onError);
server.once('listening', onSuccess);
server.listen(port);
});
}
/** /**
* Start a local server to receive the OAuth callback * Start a local server to receive the OAuth callback
* Implements automatic port fallback for Windows compatibility (issue #176)
* Returns an object with a promise and an abort function * Returns an object with a promise and an abort function
* *
* @param {string} expectedState - Expected state parameter for CSRF protection * @param {string} expectedState - Expected state parameter for CSRF protection
* @param {number} timeoutMs - Timeout in milliseconds (default 120000) * @param {number} timeoutMs - Timeout in milliseconds (default 120000)
* @returns {{promise: Promise<string>, abort: Function}} Object with promise and abort function * @returns {{promise: Promise<string>, abort: Function, getPort: Function}} Object with promise, abort, and getPort functions
*/ */
export function startCallbackServer(expectedState, timeoutMs = 120000) { export function startCallbackServer(expectedState, timeoutMs = 120000) {
let server = null; let server = null;
let timeoutId = null; let timeoutId = null;
let isAborted = false; let isAborted = false;
let actualPort = OAUTH_CONFIG.callbackPort;
const promise = new Promise(async (resolve, reject) => {
// Build list of ports to try: primary + fallbacks
const portsToTry = [OAUTH_CONFIG.callbackPort, ...(OAUTH_CONFIG.callbackFallbackPorts || [])];
const errors = [];
const promise = new Promise((resolve, reject) => {
server = http.createServer((req, res) => { server = http.createServer((req, res) => {
const url = new URL(req.url, `http://localhost:${OAUTH_CONFIG.callbackPort}`); const url = new URL(req.url, `http://localhost:${actualPort}`);
if (url.pathname !== '/oauth-callback') { if (url.pathname !== '/oauth-callback') {
res.writeHead(404); res.writeHead(404);
@@ -232,17 +260,60 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
resolve(code); resolve(code);
}); });
server.on('error', (err) => { // Try ports with fallback logic (issue #176 - Windows EACCES fix)
if (err.code === 'EADDRINUSE') { let boundSuccessfully = false;
reject(new Error(`Port ${OAUTH_CONFIG.callbackPort} is already in use. Close any other OAuth flows and try again.`)); for (const port of portsToTry) {
} else { try {
reject(err); await tryBindPort(server, port);
} actualPort = port;
}); boundSuccessfully = true;
server.listen(OAUTH_CONFIG.callbackPort, () => { if (port !== OAUTH_CONFIG.callbackPort) {
logger.info(`[OAuth] Callback server listening on port ${OAUTH_CONFIG.callbackPort}`); logger.warn(`[OAuth] Primary port ${OAUTH_CONFIG.callbackPort} unavailable, using fallback port ${port}`);
}); } else {
logger.info(`[OAuth] Callback server listening on port ${port}`);
}
break;
} catch (err) {
const errMsg = err.code === 'EACCES'
? `Permission denied on port ${port}`
: err.code === 'EADDRINUSE'
? `Port ${port} already in use`
: `Failed to bind port ${port}: ${err.message}`;
errors.push(errMsg);
logger.warn(`[OAuth] ${errMsg}`);
}
}
if (!boundSuccessfully) {
// All ports failed - provide helpful error message
const isWindows = process.platform === 'win32';
let errorMsg = `Failed to start OAuth callback server.\nTried ports: ${portsToTry.join(', ')}\n\nErrors:\n${errors.join('\n')}`;
if (isWindows) {
errorMsg += `\n
================== WINDOWS TROUBLESHOOTING ==================
The default port range may be reserved by Hyper-V/WSL2/Docker.
Option 1: Use a custom port
Set OAUTH_CALLBACK_PORT=3456 in your environment or .env file
Option 2: Reset Windows NAT (run as Administrator)
net stop winnat && net start winnat
Option 3: Check reserved port ranges
netsh interface ipv4 show excludedportrange protocol=tcp
Option 4: Exclude port from reservation (run as Administrator)
netsh int ipv4 add excludedportrange protocol=tcp startport=51121 numberofports=1
==============================================================`;
} else {
errorMsg += `\n\nTry setting a custom port: OAUTH_CALLBACK_PORT=3456`;
}
reject(new Error(errorMsg));
return;
}
// Timeout after specified duration // Timeout after specified duration
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
@@ -266,7 +337,10 @@ export function startCallbackServer(expectedState, timeoutMs = 120000) {
} }
}; };
return { promise, abort }; // Get actual port (useful when fallback is used)
const getPort = () => actualPort;
return { promise, abort, getPort };
} }
/** /**

View File

@@ -188,13 +188,19 @@ export function isThinkingModel(modelName) {
} }
// Google OAuth configuration (from opencode-antigravity-auth) // Google OAuth configuration (from opencode-antigravity-auth)
// OAuth callback port - configurable via environment variable for Windows compatibility (issue #176)
// Windows may reserve ports in range 49152-65535 for Hyper-V/WSL2/Docker, causing EACCES errors
const OAUTH_CALLBACK_PORT = parseInt(process.env.OAUTH_CALLBACK_PORT || '51121', 10);
const OAUTH_CALLBACK_FALLBACK_PORTS = [51122, 51123, 51124, 51125, 51126];
export const OAUTH_CONFIG = { export const OAUTH_CONFIG = {
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com', clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
clientSecret: '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', clientSecret: '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',
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth', authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token', tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo', userInfoUrl: 'https://www.googleapis.com/oauth2/v1/userinfo',
callbackPort: 51121, callbackPort: OAUTH_CALLBACK_PORT,
callbackFallbackPorts: OAUTH_CALLBACK_FALLBACK_PORTS,
scopes: [ scopes: [
'https://www.googleapis.com/auth/cloud-platform', 'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.email',