fix: project id forwarding regression (#93)

* fix(proxy): avoid empty project context and hydrate project_id from account token

* test(proxy): add regressions for empty project and token project_id hydration

* test(scripts): add live request validator for project_id forwarding
This commit is contained in:
vin authored and GitHub committed 2026-02-11 12:36:01 +08:00
1 parent 32abbb6793
commit ab78d93252
7 files changed
+534 -13

No files matched your search

+11 -3
View File
@@ -476,17 +476,24 @@ export class ProxyService {
private createGeminiInternalRequest(
model: string,
request: GeminiRequest,
projectId: string,
projectId: string | undefined,
requestType: string,
): GeminiInternalRequest {
return {
project: projectId,
const normalizedProjectId = projectId?.trim();
const internalRequest: GeminiInternalRequest = {
requestId: uuidv4(),
request: this.toInternalGeminiRequest(request),
model,
userAgent: process.env.PROXY_REQUEST_USER_AGENT ?? this.defaultRequestUserAgent,
requestType,
};
if (normalizedProjectId) {
internalRequest.project = normalizedProjectId;
}
return internalRequest;
}
private normalizeGeminiGenerateResponse(response: GeminiResponse): GeminiResponse {
@@ -1481,6 +1488,7 @@ export class ProxyService {
private isProjectNotFoundError(errorMessage: string): boolean {
const msg = errorMessage.toLowerCase();
return (
msg.includes('invalid project resource name projects/') ||
(msg.includes('resource projects/') && msg.includes('could not be found')) ||
(msg.includes('project') && msg.includes('not found'))
);
@@ -16,6 +16,19 @@ interface TokenData {
upstream_proxy_url?: string;
}
function normalizeProjectId(projectId: string | null | undefined): string | undefined {
if (typeof projectId !== 'string') {
return undefined;
}
const trimmedProjectId = projectId.trim();
if (trimmedProjectId === '' || /^cloud-code-\d+$/i.test(trimmedProjectId)) {
return undefined;
}
return trimmedProjectId;
}
@Injectable()
export class TokenManagerService implements OnModuleInit {
private readonly logger = new Logger(TokenManagerService.name);
@@ -177,7 +190,7 @@ export class TokenManagerService implements OnModuleInit {
tokenData.expiry_timestamp = nowSeconds + newTokens.expires_in;
// Save to DB
await this.saveRefreshedToken(accountId, tokenData);
await this.saveTokenState(accountId, tokenData);
this.tokens.set(accountId, tokenData);
this.logger.log(`Refreshed token for ${tokenData.email}`);
@@ -187,13 +200,31 @@ export class TokenManagerService implements OnModuleInit {
}
if (
typeof tokenData.project_id !== 'string' ||
tokenData.project_id.trim() === '' ||
/^cloud-code-\d+$/i.test(tokenData.project_id.trim())
normalizeProjectId(tokenData.project_id) === undefined
) {
tokenData.project_id = undefined;
}
if (!tokenData.project_id) {
try {
const fetchedProjectId = await GoogleAPIService.fetchProjectId(tokenData.access_token);
const normalizedProjectId = normalizeProjectId(fetchedProjectId);
if (normalizedProjectId) {
tokenData.project_id = normalizedProjectId;
await this.saveTokenState(accountId, tokenData);
this.tokens.set(accountId, tokenData);
this.logger.log(`Resolved project ID for ${tokenData.email}: ${normalizedProjectId}`);
} else {
this.logger.warn(
`Project ID is unavailable for ${tokenData.email}; request will continue without project context`,
);
}
} catch (error) {
this.logger.warn(`Failed to resolve project ID for ${tokenData.email}`, error);
}
}
this.logger.log(`Using account: ${tokenData.email}`);
if (sessionKey) {
this.sessionBindings.set(sessionKey, {
@@ -272,7 +303,7 @@ export class TokenManagerService implements OnModuleInit {
);
}
private async saveRefreshedToken(accountId: string, tokenData: TokenData) {
private async saveTokenState(accountId: string, tokenData: TokenData) {
try {
const acc = await CloudAccountRepo.getAccount(accountId);
if (acc && acc.token) {
@@ -281,11 +312,14 @@ export class TokenManagerService implements OnModuleInit {
access_token: tokenData.access_token,
expires_in: tokenData.expires_in,
expiry_timestamp: tokenData.expiry_timestamp,
project_id: tokenData.project_id ?? acc.token.project_id,
session_id: tokenData.session_id ?? acc.token.session_id,
upstream_proxy_url: tokenData.upstream_proxy_url ?? acc.token.upstream_proxy_url,
};
await CloudAccountRepo.updateToken(accountId, newToken);
}
} catch (e) {
this.logger.error('Unable to persist refreshed token to DB', e);
this.logger.error('Unable to persist token state to DB', e);
}
}