diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/main/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClient.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/main/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClient.java index ccdac26d7e..064ba4fccc 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/main/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClient.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/main/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClient.java @@ -315,18 +315,20 @@ private static class RetryInterceptor implements Interceptor { public Response intercept(Chain chain) throws IOException { Request request = chain.request(); Response response = null; - IOException lastException = null; + boolean responseReturned = false; try { for (int attempt = 0; attempt <= maxRetries; attempt++) { try { if (response != null) { response.close(); + response = null; } response = chain.proceed(request); // Don't retry on successful responses or client errors (4xx) if (response.isSuccessful() || response.code() < 500) { + responseReturned = true; return response; } @@ -337,7 +339,6 @@ public Response intercept(Chain chain) throws IOException { maxRetries + 1); } catch (IOException e) { - lastException = e; logger.warn( "RAGFlow request failed with exception, attempt {}/{}: {}", attempt + 1, @@ -361,14 +362,12 @@ public Response intercept(Chain chain) throws IOException { } } - if (lastException != null) { - throw lastException; - } - + responseReturned = true; return response; } finally { - // Ensure response is closed if we're not returning it successfully - if (response != null && (lastException != null || !response.isSuccessful())) { + // Keep the response open for the caller to consume, including final error + // responses. + if (response != null && !responseReturned) { response.close(); } } diff --git a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/test/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClientTest.java b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/test/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClientTest.java index 5473ff3c60..10e49fbde3 100644 --- a/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/test/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClientTest.java +++ b/agentscope-extensions/agentscope-extensions-rag/agentscope-extensions-rag-ragflow/src/test/java/io/agentscope/core/rag/integration/ragflow/RAGFlowClientTest.java @@ -17,8 +17,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.fasterxml.jackson.core.type.TypeReference; import io.agentscope.core.rag.integration.ragflow.exception.RAGFlowApiException; @@ -26,8 +32,14 @@ import io.agentscope.core.rag.integration.ragflow.model.RAGFlowResponse; import io.agentscope.core.util.JsonUtils; import java.io.IOException; +import java.lang.reflect.Field; import java.util.List; import java.util.Map; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -742,6 +754,83 @@ void testHttp503ServiceUnavailable() { assertTrue(exception.getMessage().contains("server error")); } + @Test + void testRetryKeepsFinalErrorResponseBodyReadable() { + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(500) + .setBody("{\"message\": \"first failure\"}")); + mockWebServer.enqueue( + new MockResponse() + .setResponseCode(500) + .setBody("{\"message\": \"final failure\"}")); + + RAGFlowConfig config = + RAGFlowConfig.builder() + .apiKey("test-api-key") + .baseUrl(mockWebServer.url("").toString().replaceAll("/$", "")) + .addDatasetId("dataset-123") + .maxRetries(1) + .build(); + + RAGFlowClient client = new RAGFlowClient(config); + + RAGFlowApiException exception = + assertThrows( + RAGFlowApiException.class, + () -> client.retrieve("test query", null, null, null).block()); + + assertTrue(exception.getMessage().contains("final failure")); + assertEquals(2, mockWebServer.getRequestCount()); + } + + @Test + void testRetryReturnsFinalErrorResponseAfterEarlierIOException() throws Exception { + RAGFlowConfig config = + RAGFlowConfig.builder() + .apiKey("test-api-key") + .baseUrl(mockWebServer.url("").toString().replaceAll("/$", "")) + .addDatasetId("dataset-123") + .maxRetries(1) + .build(); + RAGFlowClient client = new RAGFlowClient(config); + + Field httpClientField = RAGFlowClient.class.getDeclaredField("httpClient"); + httpClientField.setAccessible(true); + OkHttpClient httpClient = (OkHttpClient) httpClientField.get(client); + Interceptor retryInterceptor = + httpClient.interceptors().stream() + .filter( + interceptor -> + interceptor + .getClass() + .getSimpleName() + .equals("RetryInterceptor")) + .findFirst() + .orElseThrow(); + + Interceptor.Chain chain = mock(Interceptor.Chain.class); + Request request = new Request.Builder().url(mockWebServer.url("/api/v1/retrieval")).build(); + ResponseBody responseBody = mock(ResponseBody.class); + Response finalErrorResponse = mock(Response.class); + + when(chain.request()).thenReturn(request); + when(chain.proceed(request)) + .thenThrow(new IOException("first attempt failed")) + .thenReturn(finalErrorResponse); + when(finalErrorResponse.isSuccessful()).thenReturn(false); + when(finalErrorResponse.code()).thenReturn(500); + when(finalErrorResponse.body()).thenReturn(responseBody); + when(responseBody.string()).thenReturn("{\"message\": \"final failure\"}"); + + Response returned = retryInterceptor.intercept(chain); + + assertSame(finalErrorResponse, returned); + assertEquals("{\"message\": \"final failure\"}", returned.body().string()); + verify(chain, times(2)).proceed(request); + verify(finalErrorResponse, never()).close(); + } + @Test void testApiErrorWithNonZeroCode() { String errorResponse =