Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactoring Exception Handling #85

Merged
merged 4 commits into from
Dec 18, 2024
Merged
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
148 changes: 48 additions & 100 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,124 +245,72 @@ Note that we have named the annotated methods with the suffix "Basic" just to in

### Exception Handling

The `CleverClientException` provides robust error handling mechanisms for HTTP client interactions, allowing you to capture and process different types of errors comprehensively.
CleverClient provides a flexible exception handling mechanism through the `ExceptionConverter` abstract class. This allows you to convert HTTP errors and other exceptions into your own custom exceptions. Here's how to use it:

#### Key Exception Handling Scenarios
1. Create your custom HTTP exception class.
2. Create your exception converter by extending `ExceptionConverter`.
3. Implement the `convertHttpException` method to handle HTTP errors.

##### 1. Basic Exception Catching
Basic example:

```java
try {
// Your HTTP client method call
Response response = client.someMethod();
} catch (CleverClientException e) {
// Handle the specific CleverClient exception
System.err.println("An error occurred: " + e.getMessage());
}
```
// Custom HTTP Exceptions
public class FirstHttpException extends RuntimeException {
private final String errorDetail;

##### 2. Accessing HTTP Response Details

When an exception occurs during an HTTP request, you can retrieve detailed response information:

```java
try {
// HTTP client method call
client.makeRequest();
} catch (CleverClientException e) {
// Check if response info is available
e.responseInfo().ifPresent(responseInfo -> {
// Access status code
int statusCode = responseInfo.getStatusCode();
System.err.println("HTTP Status Code: " + statusCode);

// Access response headers
Map<String, List<String>> headers = responseInfo.getHeaders();
headers.forEach((key, value) ->
System.err.println(key + ": " + value)
);

// Access response data
String responseData = responseInfo.getData();
System.err.println("Response Body: " + responseData);

// Access request details
CleverClientException.HttpResponseInfo.HttpRequestInfo requestInfo =
responseInfo.getRequest();
System.err.println("Request Method: " + requestInfo.getHttpMethod());
System.err.println("Request URL: " + requestInfo.getUrl());
});
public FirstHttpException(String errorDetail) {
this.errorDetail = errorDetail;
}

// Getters
}
```
##### 3. Extracting CleverClientException from Other Exceptions

The library provides a utility method to extract `CleverClientException` from other exception types. This is the case when exceptions occurs in `CompletableFuture` processing, where a CompletionException is the outer exception and CleverClientException is the cause one:
public class SecondHttpException extends RuntimeException {
private final String errorDetail;

```java
try {
// Some method that might throw an exception
performOperation();
} catch (Throwable e) {
// Try to extract CleverClientException
Optional<CleverClientException> cleverException =
CleverClientException.getFrom(e);
public SecondHttpException(String errorDetail) {
this.errorDetail = errorDetail;
}

cleverException.ifPresent(exception -> {
// Handle the CleverClientException specifically
System.err.println("Detailed HTTP Error: " + exception.getMessage());

// Optionally access response details
exception.responseInfo().ifPresent(responseInfo -> {
// Handle response info
});
});
// Getters
}
```
#### Best Practices

- Always handle exceptions at the appropriate level of your application.
- Log detailed error information for troubleshooting.
- Use the `responseInfo()` method to get additional context about HTTP errors.
- Consider creating custom error handling logic based on specific status codes or error conditions.
// Custom Exception Converter
public class MyExceptionConverter extends ExceptionConverter {
public static void rethrow(Throwable e) {
throw new MyExceptionConverter().convert(e);
}

#### Example of Complex Error Handling
@Override
public RuntimeException convertHttpException(ResponseInfo responseInfo) {
if (responseInfo.getStatusCode() == 400) {
return new FirstHttpException(responseInfo.getData());
} else if (responseInfo.getStatusCode() == 401) {
return new SecondHttpException(responseInfo.getData());
}
}
}

```java
public void robustHttpCall() {
// Usage in try-catch block
try {
// Your CleverClient API calls here
} catch (Exception e) {
try {
// HTTP client method
client.executeRequest();
} catch (CleverClientException e) {
// Comprehensive error handling
if (e.responseInfo().isPresent()) {
HttpResponseInfo info = e.responseInfo().get();
switch (info.getStatusCode()) {
case 400:
handleBadRequest(info);
break;
case 401:
handleUnauthorized(info);
break;
case 500:
handleServerError(info);
break;
default:
handleGenericError(e);
}
} else {
// Handle exceptions without response info
handleGenericError(e);
}
MyExceptionConverter.rethrow(e);
} catch (FirstHttpException fhe) {
// Handle your first custom exception
} catch (SecondHttpException she) {
// Handle your second custom exception

// ... Other custom exceptions

} catch (RuntimeException re) {
// Handle default exceptions
}
}
```

#### Recommendations

- Always use try-catch blocks when making HTTP calls
- Log errors for monitoring and debugging
- Implement specific handling for different HTTP status codes
- Use the rich error information provided by `CleverClientException`
This mechanism allows you to handle both HTTP errors and other runtime exceptions in a clean, consistent way while preserving the original error information from the API response.

## ✳ Examples

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.sashirestela</groupId>
<artifactId>cleverclient</artifactId>
<version>1.6.1</version>
<version>1.6.2</version>
<packaging>jar</packaging>

<name>cleverclient</name>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.github.sashirestela.cleverclient.example;

import io.github.sashirestela.cleverclient.CleverClient;
import io.github.sashirestela.cleverclient.ExceptionConverter;
import io.github.sashirestela.cleverclient.ResponseInfo;
import io.github.sashirestela.cleverclient.example.openai.ChatRequest;
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 lombok.Getter;

import java.io.File;
import java.io.FileNotFoundException;
Expand All @@ -22,7 +24,7 @@
*/
public class StreamExample {

public static void main(String[] args) {
public static void main(String[] args) throws FileNotFoundException {
try {
redirectSystemErr();

Expand Down Expand Up @@ -61,7 +63,15 @@ public static void main(String[] args) {
.forEach(System.out::print);
System.out.println();
} catch (Exception e) {
handleException(e);
try {
MyExceptionConverter.rethrow(e);
} catch (MyHttpException mhe) {
System.out.println("Http Response Code: " + mhe.getResponseCode() +
"\nError Detail:\n" + mhe.getErrorDetail());
} catch (RuntimeException re) {
System.out.println(re.getMessage());
re.printStackTrace();
}
}
}

Expand All @@ -79,15 +89,33 @@ private static void showTitle(String title) {
System.out.println("-".repeat(times));
}

private static void handleException(Exception e) {
System.out.println(e.getMessage());
CleverClientException.getFrom(e)
.ifPresentOrElse(
cce -> cce.responseInfo()
.ifPresentOrElse(
System.out::println,
cce::printStackTrace),
e::printStackTrace);
public static class MyExceptionConverter extends ExceptionConverter {

private MyExceptionConverter() {
}

public static void rethrow(Throwable e) {
throw new MyExceptionConverter().convert(e);
}

@Override
public RuntimeException convertHttpException(ResponseInfo responseInfo) {
return new MyHttpException(responseInfo.getStatusCode(), responseInfo.getData());
}

}

@Getter
public static class MyHttpException extends RuntimeException {

private final int responseCode;
private final String errorDetail;

public MyHttpException(int responseCode, String errorDetail) {
this.responseCode = responseCode;
this.errorDetail = errorDetail;
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.sashirestela.cleverclient;

import io.github.sashirestela.cleverclient.support.CleverClientException;

public abstract class ExceptionConverter {

public RuntimeException convert(Throwable exception) {
var ccException = new CleverClientException(exception);
if (exception instanceof CleverClientException) {
ccException = (CleverClientException) exception;
} else if (exception.getCause() instanceof CleverClientException) {
ccException = (CleverClientException) exception.getCause();
}
var optionalResponseInfo = ccException.responseInfo();
if (optionalResponseInfo.isPresent()) {
return convertHttpException(optionalResponseInfo.get());
} else {
return (RuntimeException) exception;
}
}

public abstract RuntimeException convertHttpException(ResponseInfo responseInfo);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.github.sashirestela.cleverclient;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

/**
* Represents detailed information about an HTTP response. This inner class captures comprehensive
* details about an HTTP communication, including status code, response data, headers, and the
* corresponding request.
*/
@AllArgsConstructor
@Builder
@Data
public class ResponseInfo implements Serializable {

/** The HTTP status code of the response. */
private int statusCode;

/** The raw data received in the response. */
private String data;

/** The headers associated with the HTTP response. */
private Map<String, List<String>> headers;

/** Information about the HTTP request that generated this response. */
private RequestInfo request;

/**
* Represents details of an HTTP request. This nested inner class captures essential information
* about the HTTP request that was made, including method, URL, and headers.
*/
@AllArgsConstructor
@Builder
@Data
public static class RequestInfo implements Serializable {

/** The HTTP method used for the request (GET, POST, etc.). */
private String httpMethod;

/** The full URL of the request. */
private String url;

/** The headers sent with the HTTP request. */
private Map<String, List<String>> headers;

}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.github.sashirestela.cleverclient.sender;

import io.github.sashirestela.cleverclient.ResponseInfo;
import io.github.sashirestela.cleverclient.ResponseInfo.RequestInfo;
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 io.github.sashirestela.cleverclient.util.Constant;
Expand Down Expand Up @@ -46,8 +46,7 @@ public abstract class HttpSender {
@SuppressWarnings("unchecked")
protected void throwExceptionIfErrorIsPresent(HttpResponse<?> response, Class<?> clazz) {
logger.debug("Response Code : {}", response.statusCode());
if (CommonUtil.isBetweenHundredsOf(response.statusCode(), Constant.HTTP_CLIENT_ERROR_CODE,
Constant.HTTP_SERVER_ERROR_CODE)) {
if (!CommonUtil.isInHundredsOf(response.statusCode(), Constant.HTTP_SUCCESSFUL)) {
var data = "";
if (Stream.class.equals(clazz)) {
data = ((Stream<String>) response.body())
Expand All @@ -56,7 +55,7 @@ protected void throwExceptionIfErrorIsPresent(HttpResponse<?> response, Class<?>
try {
data = new String(((InputStream) response.body()).readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException e) {
logger.error("Cannot read input stream. {}", e.getMessage());
throw new CleverClientException(e);
}
} else {
data = (String) response.body();
Expand All @@ -66,13 +65,13 @@ protected void throwExceptionIfErrorIsPresent(HttpResponse<?> response, Class<?>
}
}

private HttpResponseInfo fillResponseInfo(HttpResponse<?> response, String data) {
private ResponseInfo fillResponseInfo(HttpResponse<?> response, String data) {
var request = response.request();
return HttpResponseInfo.builder()
return ResponseInfo.builder()
.statusCode(response.statusCode())
.data(data)
.headers(response.headers().map())
.request(HttpRequestInfo.builder()
.request(RequestInfo.builder()
.httpMethod(request.method())
.url(request.uri().toString())
.headers(request.headers().map())
Expand Down
Loading
Loading