From 481cd173818881e57b1475c23b185094238a09dc Mon Sep 17 00:00:00 2001 From: 64johnlee <64lamei@gmail.com> Date: Sat, 6 Jun 2026 21:39:40 +0800 Subject: [PATCH 1/2] fix: generate valid endpoint for global location and improve non-JSON error messages When location/region is set to 'global', the SDK was prepending 'global-' to the base path, producing 'global-aiplatform.googleapis.com' which is an invalid URL that returns a 404. New models like gemini-2.5-flash-lite require the global endpoint. Fixed in both postRequest() (post_request.ts) and ApiClient.getBaseUrl() (api_client.ts): the 'global' location now omits the region prefix, producing the correct 'aiplatform.googleapis.com'. Also fixed throwErrorIfNotOK to read the response body as text first, then parse as JSON. When the server returns an HTML error page (e.g. for the invalid 'global-aiplatform' URL), the previous code threw a confusing "Unexpected token '<'" SyntaxError. The new code surfaces a clear "Response body is not valid JSON" message with the raw body. Fixes #539 Co-Authored-By: Claude Sonnet 4.6 --- .../src/functions/post_fetch_processing.ts | 11 ++++- vertexai/src/functions/post_request.ts | 4 +- .../test/post_fetch_processing_test.ts | 46 ++++++++++++++++++- .../src/functions/test/post_request_test.ts | 25 ++++++++++ vertexai/src/resources/shared/api_client.ts | 4 +- 5 files changed, 86 insertions(+), 4 deletions(-) diff --git a/vertexai/src/functions/post_fetch_processing.ts b/vertexai/src/functions/post_fetch_processing.ts index 3d3fda83..a5f7d3ef 100644 --- a/vertexai/src/functions/post_fetch_processing.ts +++ b/vertexai/src/functions/post_fetch_processing.ts @@ -39,7 +39,16 @@ export async function throwErrorIfNotOK(response: Response | undefined) { if (!response.ok) { const status: number = response.status; const statusText: string = response.statusText; - const errorBody = await response.json(); + const responseText = await response.text(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let errorBody: any; + try { + errorBody = JSON.parse(responseText); + } catch { + throw new GoogleGenerativeAIError( + `got status: ${status} ${statusText}. Response body is not valid JSON: ${responseText.slice(0, 500)}` + ); + } const errorMessage = `got status: ${status} ${statusText}. ${JSON.stringify( errorBody )}`; diff --git a/vertexai/src/functions/post_request.ts b/vertexai/src/functions/post_request.ts index aabf3e32..738d1aa3 100644 --- a/vertexai/src/functions/post_request.ts +++ b/vertexai/src/functions/post_request.ts @@ -55,7 +55,9 @@ export async function postRequest({ requestOptions?: RequestOptions; apiVersion?: string; }): Promise { - const vertexBaseEndpoint = apiEndpoint ?? `${region}-${API_BASE_PATH}`; + const vertexBaseEndpoint = + apiEndpoint ?? + (region === 'global' ? API_BASE_PATH : `${region}-${API_BASE_PATH}`); let vertexEndpoint = `https://${vertexBaseEndpoint}/${apiVersion}/${resourcePath}:${resourceMethod}`; diff --git a/vertexai/src/functions/test/post_fetch_processing_test.ts b/vertexai/src/functions/test/post_fetch_processing_test.ts index 0ba9929a..edb801c7 100644 --- a/vertexai/src/functions/test/post_fetch_processing_test.ts +++ b/vertexai/src/functions/test/post_fetch_processing_test.ts @@ -33,7 +33,10 @@ import { UNARY_RESPONSE_MISSING_ROLE_INDEX, } from './test_data'; import * as PostFetchFunctions from '../post_fetch_processing'; -import {aggregateResponses} from '../post_fetch_processing'; +import { + aggregateResponses, + throwErrorIfNotOK, +} from '../post_fetch_processing'; import {generateContent, generateContentStream} from '../generate_content'; import {countTokens} from '../count_tokens'; @@ -180,6 +183,47 @@ describe('processStream', () => { }); }); +describe('throwErrorIfNotOK', () => { + it('non-JSON response body should throw descriptive error instead of JSON parse error', async () => { + const htmlBody = 'Not Found'; + const response = new Response(htmlBody, { + status: 404, + statusText: 'Not Found', + headers: {'Content-Type': 'text/html'}, + }); + await expectAsync(throwErrorIfNotOK(response)).toBeRejectedWithError( + /Response body is not valid JSON/ + ); + }); + + it('valid JSON 4xx response body should throw ClientError', async () => { + const errorBody = { + error: { + message: 'Resource not found', + code: 404, + status: 'NOT_FOUND', + details: [], + }, + }; + const response = new Response(JSON.stringify(errorBody), { + status: 404, + statusText: 'Not Found', + headers: {'Content-Type': 'application/json'}, + }); + await expectAsync(throwErrorIfNotOK(response)).toBeRejectedWithError( + /got status: 404/ + ); + }); + + it('OK response should not throw', async () => { + const response = new Response('{}', { + status: 200, + statusText: 'OK', + }); + await expectAsync(throwErrorIfNotOK(response)).toBeResolved(); + }); +}); + describe('processCountTokenResponse', () => { it('multiple contents, processCountTokenResponse should return faithful response', async () => { const fetchResult = new Response( diff --git a/vertexai/src/functions/test/post_request_test.ts b/vertexai/src/functions/test/post_request_test.ts index 080df448..47df4026 100644 --- a/vertexai/src/functions/test/post_request_test.ts +++ b/vertexai/src/functions/test/post_request_test.ts @@ -166,6 +166,31 @@ describe('postRequest', () => { expect(actualHeaders.get('Content-Type')).toEqual('application/json'); }); + it('global region without apiEndpoint should use base endpoint without region prefix', async () => { + await postRequest({ + region: 'global', + resourcePath: RESOURCE_PATH, + resourceMethod: RESOURCE_METHOD, + token: TOKEN, + data: data, + }); + const actualUrl: string = fetchSpy.calls.mostRecent().args[0]; + expect(actualUrl).toContain('aiplatform.googleapis.com'); + expect(actualUrl).not.toContain('global-aiplatform.googleapis.com'); + }); + + it('regional endpoint should still prepend region prefix', async () => { + await postRequest({ + region: 'us-central1', + resourcePath: RESOURCE_PATH, + resourceMethod: RESOURCE_METHOD, + token: TOKEN, + data: data, + }); + const actualUrl: string = fetchSpy.calls.mostRecent().args[0]; + expect(actualUrl).toContain('us-central1-aiplatform.googleapis.com'); + }); + it('set both custom header and apiClient, should prioritize custom headers if sent to external endpoint', async () => { const requestOptions: RequestOptions = { customHeaders: new Headers({ diff --git a/vertexai/src/resources/shared/api_client.ts b/vertexai/src/resources/shared/api_client.ts index 328884fc..35fdebb1 100644 --- a/vertexai/src/resources/shared/api_client.ts +++ b/vertexai/src/resources/shared/api_client.ts @@ -49,7 +49,9 @@ export class ApiClient { } getBaseUrl() { - return `https://${this.location}-aiplatform.googleapis.com/${this.apiVersion}`; + const locationPrefix = + this.location === 'global' ? '' : `${this.location}-`; + return `https://${locationPrefix}aiplatform.googleapis.com/${this.apiVersion}`; } getBaseResourePath() { From 9de7aa8e8d936dbeb272f526a7899702dc43fe42 Mon Sep 17 00:00:00 2001 From: 64johnlee <64lamei@gmail.com> Date: Sat, 6 Jun 2026 21:43:13 +0800 Subject: [PATCH 2/2] test: add unit tests for ApiClient.getBaseUrl with global location Co-Authored-By: Claude Sonnet 4.6 --- .../resources/shared/test/api_client_test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 vertexai/src/resources/shared/test/api_client_test.ts diff --git a/vertexai/src/resources/shared/test/api_client_test.ts b/vertexai/src/resources/shared/test/api_client_test.ts new file mode 100644 index 00000000..ca4f26e6 --- /dev/null +++ b/vertexai/src/resources/shared/test/api_client_test.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {GoogleAuth} from 'google-auth-library'; +import {ApiClient} from '../api_client'; + +function makeClient(location: string): ApiClient { + return new ApiClient( + 'test-project', + location, + 'v1', + {} as unknown as GoogleAuth + ); +} + +describe('ApiClient.getBaseUrl', () => { + it('global location should return endpoint without region prefix', () => { + const client = makeClient('global'); + expect(client.getBaseUrl()).toBe('https://aiplatform.googleapis.com/v1'); + expect(client.getBaseUrl()).not.toContain('global-'); + }); + + it('regional location should return endpoint with region prefix', () => { + const client = makeClient('us-central1'); + expect(client.getBaseUrl()).toBe( + 'https://us-central1-aiplatform.googleapis.com/v1' + ); + }); + + it('eu-west4 location should return endpoint with correct region prefix', () => { + const client = makeClient('eu-west4'); + expect(client.getBaseUrl()).toBe( + 'https://eu-west4-aiplatform.googleapis.com/v1' + ); + }); +});