Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/src/main/java/com/google/adk/sessions/ApiResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public abstract class ApiResponse implements AutoCloseable {
/** Gets the HttpEntity. */
public abstract ResponseBody getResponseBody();

/** Gets the HTTP status code of the response. */
public abstract int getStatusCode();

@Override
public abstract void close();
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ public ResponseBody getResponseBody() {
return response.body();
}

/** Returns the HTTP status code from the response. */
@Override
public int getStatusCode() {
return response.code();
}

/** Closes the Http response. */
@Override
public void close() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2025 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
*
* http://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.
*/

package com.google.adk.sessions;

/**
* Signals a non-2xx, non-404 HTTP status from the Vertex AI Session API. Extends {@link
* SessionException} so existing {@code catch (SessionException)} call sites still work.
*/
public final class VertexAiApiException extends SessionException {
private final int statusCode;

VertexAiApiException(int statusCode, String responseBody) {
super(
"Vertex AI Session API request failed with HTTP status "
+ statusCode
+ (responseBody == null || responseBody.isEmpty() ? "" : ": " + responseBody));
this.statusCode = statusCode;
}

/** Returns the HTTP status code returned by the API. */
public int statusCode() {
return statusCode;
}
}
25 changes: 19 additions & 6 deletions core/src/main/java/com/google/adk/sessions/VertexAiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,29 @@ private Single<ApiResponse> performApiRequest(String method, String path, String
*/
@Nullable
private static Maybe<JsonNode> getJsonResponse(ApiResponse apiResponse) {
if (apiResponse == null) {
return Maybe.empty();
}
try {
if (apiResponse == null || apiResponse.getResponseBody() == null) {
int statusCode = apiResponse.getStatusCode();
String responseString;
try {
ResponseBody responseBody = apiResponse.getResponseBody();
responseString = responseBody == null ? "" : responseBody.string();
} catch (IOException e) {
return Maybe.error(new UncheckedIOException(e));
}

if (statusCode == 404) {
return Maybe.empty();
}
if (statusCode < 200 || statusCode >= 300) {
return Maybe.error(new VertexAiApiException(statusCode, responseString));
}
if (responseString.isEmpty()) {
return Maybe.empty();
}
try {
ResponseBody responseBody = apiResponse.getResponseBody();
String responseString = responseBody.string(); // Read body here
if (responseString.isEmpty()) {
return Maybe.empty();
}
return Maybe.just(objectMapper.readTree(responseString));
} catch (IOException e) {
return Maybe.error(new UncheckedIOException(e));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,12 @@ private ListEventsResponse parseListEventsResponse(JsonNode listEventsResponse)
.build();
}

/**
* {@inheritDoc}
*
* <p>On a non-2xx, non-404 HTTP response the returned {@link Maybe} emits a {@link
* VertexAiApiException}.
*/
@Override
public Maybe<Session> getSession(
String appName, String userId, String sessionId, Optional<GetSessionConfig> config) {
Expand Down
13 changes: 11 additions & 2 deletions core/src/test/java/com/google/adk/sessions/MockApiAnswer.java
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,19 @@ public ApiResponse answer(InvocationOnMock invocation) throws Throwable {
}

private static ApiResponse responseWithBody(String body) {
return responseWithStatus(200, body);
}

static ApiResponse responseWithStatus(int statusCode, String body) {
return new ApiResponse() {
@Override
public ResponseBody getResponseBody() {
return ResponseBody.create(JSON_MEDIA_TYPE, body);
return body == null ? null : ResponseBody.create(JSON_MEDIA_TYPE, body);
}

@Override
public int getStatusCode() {
return statusCode;
}

@Override
Expand Down Expand Up @@ -144,7 +153,7 @@ private ApiResponse handleGetSession(String path) throws Exception {
if (sessionData != null) {
return responseWithBody(sessionData);
} else {
throw new RuntimeException("Session not found: " + sessionId);
return responseWithStatus(404, "");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,10 @@ public void createSession_noState_success() throws Exception {
}

@Test
public void getEmptySession_success() {
RuntimeException exception =
assertThrows(
RuntimeException.class,
() ->
vertexAiSessionService
.getSession("123", "user", "0", Optional.empty())
.blockingGet());
assertThat(exception).hasMessageThat().contains("Session not found: 0");
public void getEmptySession_returnsNull() {
Session session =
vertexAiSessionService.getSession("123", "user", "0", Optional.empty()).blockingGet();
assertThat(session).isNull();
}

@Test
Expand All @@ -258,14 +253,41 @@ public void getAndDeleteSession_success() throws Exception {
vertexAiSessionService.getSession("123", "user", "1", Optional.empty()).blockingGet();
assertThat(session.toJson()).isEqualTo(getMockSession().toJson());
vertexAiSessionService.deleteSession("123", "user", "1").blockingAwait();
RuntimeException exception =
Session sessionAfterDelete =
vertexAiSessionService.getSession("123", "user", "1", Optional.empty()).blockingGet();
assertThat(sessionAfterDelete).isNull();
}

@Test
public void getSession_permissionDenied_propagatesAsError() {
when(mockApiClient.request(eq("GET"), eq("reasoningEngines/123/sessions/1"), eq("")))
.thenReturn(
MockApiAnswer.responseWithStatus(
403, "{\"userId\": \"user\", \"error\": \"permission denied\"}"));

VertexAiApiException exception =
assertThrows(
VertexAiApiException.class,
() ->
vertexAiSessionService
.getSession("123", "user", "1", Optional.empty())
.blockingGet());
assertThat(exception.statusCode()).isEqualTo(403);
}

@Test
public void getSession_serverError_propagatesAsError() {
when(mockApiClient.request(eq("GET"), eq("reasoningEngines/123/sessions/1"), eq("")))
.thenReturn(MockApiAnswer.responseWithStatus(500, "{\"error\": \"internal\"}"));

VertexAiApiException exception =
assertThrows(
RuntimeException.class,
VertexAiApiException.class,
() ->
vertexAiSessionService
.getSession("123", "user", "1", Optional.empty())
.blockingGet());
assertThat(exception).hasMessageThat().contains("Session not found: 1");
assertThat(exception.statusCode()).isEqualTo(500);
}

@Test
Expand Down
Loading