Skip to content

Commit 9318fdb

Browse files
Copilotlpcox
andauthored
fix(api-proxy): strip all Gemini auth query param variants to prevent API_KEY_INVALID (#2182)
* Initial plan * fix: strip apiKey/api_key Gemini params and add startup logging Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/0cf58c7b-8967-48bd-8a30-c02adabfd656 * fix: update stale comments and add missing 6th test case Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/46aa116c-47a9-4f66-8e20-13cb6f683aab Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent c7d506a commit 9318fdb

2 files changed

Lines changed: 43 additions & 9 deletions

File tree

containers/api-proxy/server.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -190,16 +190,16 @@ function buildUpstreamPath(reqUrl, targetHost, basePath) {
190190
}
191191

192192
/**
193-
* Strip the `key` query parameter from a Gemini request URL.
193+
* Strip all known Gemini API-key query parameters from a request URL.
194194
*
195-
* The @google/genai SDK (and older Gemini SDK versions) may append `?key=<value>`
196-
* to every request URL in addition to setting the `x-goog-api-key` header.
197-
* The proxy injects the real key via the header, so the placeholder `key=`
198-
* value must be removed before forwarding to Google to prevent
199-
* API_KEY_INVALID errors.
195+
* The @google/genai SDK (and older Gemini SDK versions) may append auth params
196+
* (`?key=`, `?apiKey=`, or `?api_key=`) to every request URL in addition to
197+
* setting the `x-goog-api-key` header. The proxy injects the real key via the
198+
* header, so any placeholder param must be removed before forwarding to Google
199+
* to prevent API_KEY_INVALID errors.
200200
*
201201
* @param {string} reqUrl - The incoming request URL (must start with exactly one '/')
202-
* @returns {string} URL with the `key` query parameter removed
202+
* @returns {string} URL with all Gemini auth query parameters removed
203203
*/
204204
function stripGeminiKeyParam(reqUrl) {
205205
// Only operate on relative request paths that begin with exactly one slash.
@@ -211,6 +211,8 @@ function stripGeminiKeyParam(reqUrl) {
211211
}
212212
const parsed = new URL(reqUrl, 'http://localhost');
213213
parsed.searchParams.delete('key');
214+
parsed.searchParams.delete('apiKey');
215+
parsed.searchParams.delete('api_key');
214216
// Reconstruct relative path only — never emit the scheme/host from the dummy base.
215217
return parsed.pathname + parsed.search;
216218
}
@@ -1355,7 +1357,7 @@ if (require.main === module) {
13551357
const contentLength = parseInt(req.headers['content-length'], 10) || 0;
13561358
if (checkRateLimit(req, res, 'gemini', contentLength)) return;
13571359

1358-
// Strip any ?key= query parameter — the @google/genai SDK may append it to the URL.
1360+
// Strip any auth query params (?key=, ?apiKey=, ?api_key=) — the SDK may append them.
13591361
// The proxy injects the real key via x-goog-api-key header instead.
13601362
req.url = stripGeminiKeyParam(req.url);
13611363

@@ -1365,13 +1367,14 @@ if (require.main === module) {
13651367
});
13661368

13671369
geminiServer.on('upgrade', (req, socket, head) => {
1368-
// Strip any ?key= query parameter — the @google/genai SDK may append it to the URL.
1370+
// Strip any auth query params (?key=, ?apiKey=, ?api_key=) — the SDK may append them.
13691371
req.url = stripGeminiKeyParam(req.url);
13701372
proxyWebSocket(req, socket, head, GEMINI_API_TARGET, {
13711373
'x-goog-api-key': GEMINI_API_KEY,
13721374
}, 'gemini', GEMINI_API_BASE_PATH);
13731375
});
13741376

1377+
logRequest('info', 'server_start', { message: `GEMINI_API_KEY configured (length=${GEMINI_API_KEY.length})` });
13751378
geminiServer.listen(10003, '0.0.0.0', () => {
13761379
logRequest('info', 'server_start', { message: 'Google Gemini proxy listening on port 10003', target: GEMINI_API_TARGET });
13771380
onListenerReady();
@@ -1395,6 +1398,7 @@ if (require.main === module) {
13951398
socket.destroy();
13961399
});
13971400

1401+
logRequest('warn', 'server_start', { message: 'GEMINI_API_KEY not set — Gemini proxy will return 503' });
13981402
geminiServer.listen(10003, '0.0.0.0', () => {
13991403
logRequest('info', 'server_start', { message: 'Gemini endpoint listening on port 10003 (Gemini not configured — returning 503)' });
14001404
});

containers/api-proxy/server.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,36 @@ describe('stripGeminiKeyParam', () => {
540540
const result = stripGeminiKeyParam('/v1/generateContent?key=abc');
541541
expect(result).toBe('/v1/generateContent');
542542
});
543+
544+
it('should remove the apiKey= query parameter', () => {
545+
expect(stripGeminiKeyParam('/v1/models/gemini-pro:generateContent?apiKey=placeholder'))
546+
.toBe('/v1/models/gemini-pro:generateContent');
547+
});
548+
549+
it('should remove the api_key= query parameter', () => {
550+
expect(stripGeminiKeyParam('/v1/models/gemini-pro:generateContent?api_key=placeholder'))
551+
.toBe('/v1/models/gemini-pro:generateContent');
552+
});
553+
554+
it('should remove apiKey= while preserving other query parameters', () => {
555+
expect(stripGeminiKeyParam('/v1/models/gemini-pro:generateContent?apiKey=placeholder&alt=json'))
556+
.toBe('/v1/models/gemini-pro:generateContent?alt=json');
557+
});
558+
559+
it('should remove api_key= while preserving other query parameters', () => {
560+
expect(stripGeminiKeyParam('/v1/models/gemini-pro:generateContent?api_key=placeholder&alt=json'))
561+
.toBe('/v1/models/gemini-pro:generateContent?alt=json');
562+
});
563+
564+
it('should remove all auth params when multiple variants are present', () => {
565+
expect(stripGeminiKeyParam('/v1/models/gemini-pro:generateContent?key=foo&apiKey=bar&api_key=baz&alt=json'))
566+
.toBe('/v1/models/gemini-pro:generateContent?alt=json');
567+
});
568+
569+
it('should handle path with only api_key= param, leaving no trailing ?', () => {
570+
const result = stripGeminiKeyParam('/v1/generateContent?api_key=abc');
571+
expect(result).toBe('/v1/generateContent');
572+
});
543573
});
544574

545575
// ── Helpers for proxyWebSocket tests ──────────────────────────────────────────

0 commit comments

Comments
 (0)