diff --git a/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java b/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java index b9770ce..2d4e40a 100644 --- a/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java +++ b/src/example/java/io/github/sashirestela/cleverclient/example/StreamExample.java @@ -5,6 +5,7 @@ import io.github.sashirestela.cleverclient.example.openai.ChatResponse; import io.github.sashirestela.cleverclient.example.openai.ChatService; import io.github.sashirestela.cleverclient.example.openai.Message; +import io.github.sashirestela.cleverclient.support.CleverClientException; import java.util.Arrays; @@ -12,46 +13,50 @@ * Before running this example you must have an OpenAI account and keep your Api Key in an * environment variable called OPENAI_API_KEY. * - * @see OpenAI + * @see OpenAI * Authentication */ public class StreamExample { public static void main(String[] args) { - final var BASE_URL = "https://api.openai.com"; - final var AUTHORIZATION_HEADER = "Authorization"; - final var BEARER_AUTHORIZATION = "Bearer " + System.getenv("OPENAI_API_KEY"); - final var END_OF_STREAM = "[DONE]"; + try { + final var BASE_URL = "https://api.openai.com"; + final var AUTHORIZATION_HEADER = "Authorization"; + final var BEARER_AUTHORIZATION = "Bearer " + System.getenv("OPENAI_API_KEY"); + final var END_OF_STREAM = "[DONE]"; - var cleverClient = CleverClient.builder() - .baseUrl(BASE_URL) - .header(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION) - .endOfStream(END_OF_STREAM) - .build(); - var chatService = cleverClient.create(ChatService.class); + var cleverClient = CleverClient.builder() + .baseUrl(BASE_URL) + .header(AUTHORIZATION_HEADER, BEARER_AUTHORIZATION) + .endOfStream(END_OF_STREAM) + .build(); + var chatService = cleverClient.create(ChatService.class); - var chatRequest = ChatRequest.builder() - .model("gpt-3.5-turbo") - .messages(Arrays.asList( - new Message("user", "Write an article about AI, no more than 100 words."))) - .temperature(0.7) - .build(); + var chatRequest = ChatRequest.builder() + .model("gpt-3.5-turbo") + .messages(Arrays.asList( + new Message("user", "Write an article about AI, no more than 100 words."))) + .temperature(0.7) + .build(); - showTitle("Example Create Synchronous Stream"); - var chatResponseSync = chatService.createSyncStream(chatRequest); - chatResponseSync - .filter(chatResp -> chatResp.firstContent() != null) - .map(ChatResponse::firstContent) - .forEach(System.out::print); - System.out.println(); + showTitle("Example Create Synchronous Stream"); + var chatResponseSync = chatService.createSyncStream(chatRequest); + chatResponseSync + .filter(chatResp -> chatResp.firstContent() != null) + .map(ChatResponse::firstContent) + .forEach(System.out::print); + System.out.println(); - showTitle("Example Create Asynchronous Stream"); - var chatResponseAsync = chatService.createAsyncStream(chatRequest).join(); - chatResponseAsync - .filter(chatResp -> chatResp.firstContent() != null) - .map(ChatResponse::firstContent) - .forEach(System.out::print); - System.out.println(); + showTitle("Example Create Asynchronous Stream"); + var chatResponseAsync = chatService.createAsyncStream(chatRequest).join(); + chatResponseAsync + .filter(chatResp -> chatResp.firstContent() != null) + .map(ChatResponse::firstContent) + .forEach(System.out::print); + System.out.println(); + } catch (Exception e) { + handleException(e); + } } private static void showTitle(String title) { @@ -61,4 +66,19 @@ private static void showTitle(String title) { System.out.println("-".repeat(times)); } + private static void handleException(Exception e) { + System.out.println(e.getMessage()); + CleverClientException cce = null; + if (e instanceof CleverClientException) { + cce = (CleverClientException) e; + } else if (e.getCause() instanceof CleverClientException) { + cce = (CleverClientException) e.getCause(); + } + if (cce != null) { + cce.responseInfo().ifPresentOrElse(System.out::println, cce::printStackTrace); + } else { + e.printStackTrace(); + } + } + } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java index 4758954..e50ff67 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSender.java @@ -1,6 +1,8 @@ package io.github.sashirestela.cleverclient.sender; import io.github.sashirestela.cleverclient.support.CleverClientException; +import io.github.sashirestela.cleverclient.support.CleverClientException.HttpResponseInfo; +import io.github.sashirestela.cleverclient.support.CleverClientException.HttpResponseInfo.HttpRequestInfo; import io.github.sashirestela.cleverclient.support.ReturnType; import io.github.sashirestela.cleverclient.util.CommonUtil; import org.slf4j.Logger; @@ -59,8 +61,22 @@ protected void throwExceptionIfErrorIsPresent(HttpResponse response, Class data = (String) response.body(); } logger.error("Response : {}", data); - throw new CleverClientException("ERROR : {0}", data, null); + throw new CleverClientException(fillResponseInfo(response, data)); } } + private HttpResponseInfo fillResponseInfo(HttpResponse response, String data) { + var request = response.request(); + return HttpResponseInfo.builder() + .statusCode(response.statusCode()) + .data(data) + .headers(response.headers().map()) + .request(HttpRequestInfo.builder() + .httpMethod(request.method()) + .url(request.uri().toString()) + .headers(request.headers().map()) + .build()) + .build(); + } + } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java index c10e9db..875d1ef 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncBinarySender.java @@ -27,7 +27,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Return } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); - throw new CleverClientException(e.getMessage(), null, e); + throw new CleverClientException(e); } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncCustomSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncCustomSender.java index 2f125cb..960e0b8 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncCustomSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncCustomSender.java @@ -27,7 +27,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Return } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); - throw new CleverClientException(e.getMessage(), null, e); + throw new CleverClientException(e); } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java index e5c9a72..6789470 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncGenericSender.java @@ -28,7 +28,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Return } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); - throw new CleverClientException(e.getMessage(), null, e); + throw new CleverClientException(e); } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java index 420928c..3ec33b1 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncListSender.java @@ -27,7 +27,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Return } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); - throw new CleverClientException(e.getMessage(), null, e); + throw new CleverClientException(e); } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java index 6c24481..3bd0adf 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncPlainTextSender.java @@ -26,7 +26,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Return } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); - throw new CleverClientException(e.getMessage(), null, e); + throw new CleverClientException(e); } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamEventSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamEventSender.java index af55b07..5b69710 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamEventSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamEventSender.java @@ -41,7 +41,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Return } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); - throw new CleverClientException(e.getMessage(), null, e); + throw new CleverClientException(e); } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java index f8bd923..2e2146f 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java +++ b/src/main/java/io/github/sashirestela/cleverclient/sender/HttpSyncStreamSender.java @@ -35,7 +35,7 @@ public Object sendRequest(HttpClient httpClient, HttpRequest httpRequest, Return } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); - throw new CleverClientException(e.getMessage(), null, e); + throw new CleverClientException(e); } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java b/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java index 7f4999b..ebd5753 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java +++ b/src/main/java/io/github/sashirestela/cleverclient/support/CleverClientException.java @@ -1,17 +1,64 @@ package io.github.sashirestela.cleverclient.support; +import io.github.sashirestela.cleverclient.util.Constant; +import lombok.Builder; +import lombok.Data; + +import java.io.Serializable; import java.text.MessageFormat; import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; public class CleverClientException extends RuntimeException { + private final HttpResponseInfo responseInfo; + public CleverClientException(String message) { super(message); + this.responseInfo = null; + } + + public CleverClientException(Throwable cause) { + super(cause); + this.responseInfo = null; } public CleverClientException(String message, Object... parameters) { super(MessageFormat.format(message, Arrays.copyOfRange(parameters, 0, parameters.length - 1)), (Throwable) parameters[parameters.length - 1]); + this.responseInfo = null; + } + + public CleverClientException(HttpResponseInfo responseInfo) { + super(MessageFormat.format(Constant.HTTP_ERROR_MESSAGE, responseInfo.getStatusCode()), null); + this.responseInfo = responseInfo; + } + + public Optional responseInfo() { + return Optional.ofNullable(responseInfo); + } + + @Data + @Builder + public static class HttpResponseInfo implements Serializable { + + private int statusCode; + private String data; + private Map> headers; + private HttpRequestInfo request; + + @Data + @Builder + public static class HttpRequestInfo implements Serializable { + + private String httpMethod; + private String url; + private Map> headers; + + } + } } diff --git a/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java b/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java index f589cae..f3c9cbd 100644 --- a/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java +++ b/src/main/java/io/github/sashirestela/cleverclient/util/Constant.java @@ -12,4 +12,6 @@ private Constant() { public static final String REGEX_PATH_PARAM_URL = "\\{(.*?)\\}"; + public static final String HTTP_ERROR_MESSAGE = "HTTP interaction failed: server returned a {0} response status."; + } diff --git a/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java b/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java index acc9907..221afaf 100644 --- a/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java +++ b/src/test/java/io/github/sashirestela/cleverclient/http/HttpProcessorTest.java @@ -14,16 +14,21 @@ import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; import java.net.http.HttpClient; +import java.net.http.HttpHeaders; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Paths; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -39,6 +44,8 @@ class HttpProcessorTest { HttpResponse httpResponse = mock(HttpResponse.class); HttpResponse> httpResponseStream = mock(HttpResponse.class); HttpResponse httpResponseBinary = mock(HttpResponse.class); + HttpRequest httpRequest = mock(HttpRequest.class); + HttpHeaders httpHeaders = mock(HttpHeaders.class); @BeforeAll static void setup() { @@ -359,49 +366,76 @@ void shouldReturnAnObjectWhenMethodIsAnnotatedWithMultipart() { } @Test - void shouldThrownExceptionWhenCallingNoStreamingMethodAndServerRespondsWithError() { + void shouldThrownExceptionWhenCallingNoStreamingMethodAndServerRespondsWithError() throws URISyntaxException { when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofString().getClass()))) .thenReturn(CompletableFuture.completedFuture(httpResponse)); + when(httpHeaders.map()).thenReturn(Map.of()); + when(httpRequest.method()).thenReturn("GET"); + when(httpRequest.uri()).thenReturn(new URI("https://api.com")); + when(httpRequest.headers()).thenReturn(httpHeaders); when(httpResponse.statusCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); when(httpResponse.body()).thenReturn( "{\"error\": {\"message\": \"The resource does not exist\", \"type\": \"T\", \"param\": \"P\", \"code\": \"C\"}}"); + when(httpResponse.headers()).thenReturn(httpHeaders); + when(httpResponse.request()).thenReturn(httpRequest); var service = httpProcessor.createProxy(ITest.AsyncService.class); var futureService = service.getDemo(100); - Exception exception = assertThrows(CompletionException.class, - () -> futureService.join()); - assertTrue(exception.getMessage().contains("The resource does not exist")); + + Exception exception = assertThrows(CompletionException.class, () -> futureService.join()); + CleverClientException nestedException = (CleverClientException) exception.getCause(); + assertNotNull(nestedException); + assertEquals(CleverClientException.class, nestedException.getClass()); + assertTrue(nestedException.responseInfo().get().getData().contains("The resource does not exist")); } @Test - void shouldThrownExceptionWhenCallingStreamingMethodAndServerRespondsWithError() { + void shouldThrownExceptionWhenCallingStreamingMethodAndServerRespondsWithError() throws URISyntaxException { when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofLines().getClass()))) .thenReturn(CompletableFuture.completedFuture(httpResponseStream)); + when(httpHeaders.map()).thenReturn(Map.of()); + when(httpRequest.method()).thenReturn("GET"); + when(httpRequest.uri()).thenReturn(new URI("https://api.com")); + when(httpRequest.headers()).thenReturn(httpHeaders); when(httpResponseStream.statusCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); when(httpResponseStream.body()).thenReturn(Stream.of( "{\"error\": {\"message\": \"The resource does not exist\", \"type\": \"T\", \"param\": \"P\", \"code\": \"C\"}}")); + when(httpResponseStream.headers()).thenReturn(httpHeaders); + when(httpResponseStream.request()).thenReturn(httpRequest); var service = httpProcessor.createProxy(ITest.AsyncService.class); var futureService = service.getDemoStream(new ITest.RequestDemo("Descr", null)); - Exception exception = assertThrows(CompletionException.class, - () -> futureService.join()); - assertTrue(exception.getMessage().contains("The resource does not exist")); + + Exception exception = assertThrows(CompletionException.class, () -> futureService.join()); + CleverClientException nestedException = (CleverClientException) exception.getCause(); + assertNotNull(nestedException); + assertEquals(CleverClientException.class, nestedException.getClass()); + assertTrue(nestedException.responseInfo().get().getData().contains("The resource does not exist")); } @Test - void shouldThrownExceptionWhenCallingBinaryMethodAndServerRespondsWithError() { + void shouldThrownExceptionWhenCallingBinaryMethodAndServerRespondsWithError() throws URISyntaxException { when(httpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandlers.ofInputStream().getClass()))) .thenReturn(CompletableFuture.completedFuture(httpResponseBinary)); + when(httpHeaders.map()).thenReturn(Map.of()); + when(httpRequest.method()).thenReturn("GET"); + when(httpRequest.uri()).thenReturn(new URI("https://api.com")); + when(httpRequest.headers()).thenReturn(httpHeaders); when(httpResponseBinary.statusCode()).thenReturn(HttpURLConnection.HTTP_NOT_FOUND); when(httpResponseBinary.body()).thenReturn(new ByteArrayInputStream( "{\"error\": {\"message\": \"The resource does not exist\", \"type\": \"T\", \"param\": \"P\", \"code\": \"C\"}}" .getBytes())); + when(httpResponseBinary.headers()).thenReturn(httpHeaders); + when(httpResponseBinary.request()).thenReturn(httpRequest); var service = httpProcessor.createProxy(ITest.AsyncService.class); var futureService = service.getDemoBinary(100); - Exception exception = assertThrows(CompletionException.class, - () -> futureService.join()); - assertTrue(exception.getMessage().contains("The resource does not exist")); + + Exception exception = assertThrows(CompletionException.class, () -> futureService.join()); + CleverClientException nestedException = (CleverClientException) exception.getCause(); + assertNotNull(nestedException); + assertEquals(CleverClientException.class, nestedException.getClass()); + assertTrue(nestedException.responseInfo().get().getData().contains("The resource does not exist")); } @Test