From 0076d573c82adcf7867518eae06189d86ed53e9b Mon Sep 17 00:00:00 2001 From: t31a Date: Thu, 9 Apr 2026 15:00:24 -0300 Subject: [PATCH 1/4] fix: harden retry and response conversion handling --- .../com/asaas/apisdk/http/ModelConverter.java | 43 +++++++++++++------ .../http/interceptors/RetryInterceptor.java | 11 +++-- .../asaas/apisdk/services/BaseService.java | 10 +++-- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/asaas/apisdk/http/ModelConverter.java b/src/main/java/com/asaas/apisdk/http/ModelConverter.java index d74ff42..f44ca09 100644 --- a/src/main/java/com/asaas/apisdk/http/ModelConverter.java +++ b/src/main/java/com/asaas/apisdk/http/ModelConverter.java @@ -2,13 +2,13 @@ package com.asaas.apisdk.http; +import com.asaas.apisdk.exceptions.ApiError; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import okhttp3.Response; -import okhttp3.ResponseBody; import org.openapitools.jackson.nullable.JsonNullableModule; public final class ModelConverter { @@ -30,12 +30,10 @@ public final class ModelConverter { private ModelConverter() {} public static T convert(final Response response, final Class clazz) { - final ResponseBody body = response.body(); try { - return mapper.readValue(body.string(), clazz); + return mapper.readValue(readBodyAsString(response), clazz); } catch (Exception e) { - e.printStackTrace(); - return null; + throw buildConversionApiError(response, String.format("deserialize response body to %s", clazz.getSimpleName()), e); } } @@ -50,10 +48,9 @@ public static T convert(final String response, final Class clazz) { public static T convert(Response response, TypeReference typeReference) { try { - return convert(response.body().string(), typeReference); + return mapper.readValue(readBodyAsString(response), typeReference); } catch (Exception e) { - e.printStackTrace(); - return null; + throw buildConversionApiError(response, "deserialize response body", e); } } @@ -68,19 +65,20 @@ public static T convert(String response, TypeReference typeReference) { public static String readString(Response response) { try { - return response.body().string(); + return readBodyAsString(response); } catch (Exception e) { - e.printStackTrace(); - return null; + throw buildConversionApiError(response, "read response body as string", e); } } public static byte[] readBytes(Response response) { try { + if (response.body() == null) { + throw new IllegalStateException("Response body is empty"); + } return response.body().bytes(); } catch (Exception e) { - e.printStackTrace(); - return null; + throw buildConversionApiError(response, "read response body as bytes", e); } } @@ -92,4 +90,23 @@ public static String modelToJson(final Object model) { return null; } } + + private static String readBodyAsString(Response response) throws Exception { + if (response.body() == null) { + throw new IllegalStateException("Response body is empty"); + } + + return response.body().string(); + } + + private static ApiError buildConversionApiError(Response response, String action, Exception cause) { + int status = response != null ? response.code() : 0; + String url = response != null && response.request() != null ? response.request().url().toString() : "unknown"; + + String message = String.format("Failed to %s (status: %d, url: %s)", action, status, url); + // TODO: Consider a dedicated response-conversion exception type for malformed 2xx payloads. + ApiError error = new ApiError(message, status, response); + error.initCause(cause); + return error; + } } diff --git a/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java b/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java index 61c8896..4ae41c6 100644 --- a/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java +++ b/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java @@ -28,16 +28,17 @@ public Response intercept(Chain chain) throws IOException { try { response = chain.proceed(request); - if (!isRetryable(response)) { + if (!isRetryable(response) || tryCount == config.getMaxRetries()) { return response; } - tryCount++; } catch (IOException e) { - if (!config.getExceptionsToRetry().contains(e.getClass()) || tryCount == config.getMaxRetries()) { + if (!isRetryableException(e) || tryCount == config.getMaxRetries()) { throw e; } } + tryCount++; + final int delay = calculateDelay(tryCount); try { Thread.sleep(delay); @@ -50,6 +51,10 @@ public Response intercept(Chain chain) throws IOException { return response; } + private boolean isRetryableException(IOException e) { + return config.getExceptionsToRetry().stream().anyMatch(retryableException -> retryableException.isInstance(e)); + } + private int calculateDelay(int tryCount) { final int delay = (int) (config.getInitialDelay() * Math.pow(config.getBackoffFactor(), tryCount - 1)); return Math.min(delay, config.getMaxDelay()); diff --git a/src/main/java/com/asaas/apisdk/services/BaseService.java b/src/main/java/com/asaas/apisdk/services/BaseService.java index 490d823..d1997e4 100644 --- a/src/main/java/com/asaas/apisdk/services/BaseService.java +++ b/src/main/java/com/asaas/apisdk/services/BaseService.java @@ -6,8 +6,6 @@ import com.asaas.apisdk.exceptions.ApiError; import com.asaas.apisdk.http.Environment; import com.asaas.apisdk.http.ModelConverter; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.lang.reflect.Constructor; import java.net.SocketTimeoutException; @@ -82,6 +80,10 @@ private String extractErrorMessage(Response response, Object errorModel) { return message; } + private ApiError buildGenericApiError(Response response) { + return new ApiError(extractErrorMessage(response, null), response.code(), response); + } + protected Response execute(Request request) throws ApiError { Response response; try { @@ -128,7 +130,7 @@ protected Response execute(Request request) throws ApiError { } // If no specific error model is mapped or conversion failed, throw generic ApiError - throw new ApiError(extractErrorMessage(response, null), response.code(), response); + throw buildGenericApiError(response); } protected CompletableFuture executeAsync(Request request) { @@ -167,7 +169,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) { } // If no specific error model is mapped or conversion failed, throw generic ApiError - ApiError error = new ApiError(extractErrorMessage(response, null), response.code(), response); + ApiError error = buildGenericApiError(response); future.completeExceptionally(error); return; } From 23fb18fbf72400ccacef50baf74781f49345d2cc Mon Sep 17 00:00:00 2001 From: t31a Date: Thu, 9 Apr 2026 15:18:08 -0300 Subject: [PATCH 2/4] correct bug fix only change other things are not really needed for correct behaviour with little change --- .../asaas/apisdk/http/interceptors/RetryInterceptor.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java b/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java index 4ae41c6..b09e88d 100644 --- a/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java +++ b/src/main/java/com/asaas/apisdk/http/interceptors/RetryInterceptor.java @@ -32,7 +32,7 @@ public Response intercept(Chain chain) throws IOException { return response; } } catch (IOException e) { - if (!isRetryableException(e) || tryCount == config.getMaxRetries()) { + if (!config.getExceptionsToRetry().contains(e.getClass()) || tryCount == config.getMaxRetries()) { throw e; } } @@ -50,11 +50,6 @@ public Response intercept(Chain chain) throws IOException { return response; } - - private boolean isRetryableException(IOException e) { - return config.getExceptionsToRetry().stream().anyMatch(retryableException -> retryableException.isInstance(e)); - } - private int calculateDelay(int tryCount) { final int delay = (int) (config.getInitialDelay() * Math.pow(config.getBackoffFactor(), tryCount - 1)); return Math.min(delay, config.getMaxDelay()); From c452dac9f96fb4c31adef606a2ab4d666cdcf958 Mon Sep 17 00:00:00 2001 From: t31a Date: Thu, 9 Apr 2026 15:22:43 -0300 Subject: [PATCH 3/4] fix: limit retry correction scope --- .../com/asaas/apisdk/http/ModelConverter.java | 39 +++++++------------ .../asaas/apisdk/services/BaseService.java | 10 ++--- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/asaas/apisdk/http/ModelConverter.java b/src/main/java/com/asaas/apisdk/http/ModelConverter.java index f44ca09..c2b138a 100644 --- a/src/main/java/com/asaas/apisdk/http/ModelConverter.java +++ b/src/main/java/com/asaas/apisdk/http/ModelConverter.java @@ -2,13 +2,13 @@ package com.asaas.apisdk.http; -import com.asaas.apisdk.exceptions.ApiError; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import okhttp3.Response; +import okhttp3.ResponseBody; import org.openapitools.jackson.nullable.JsonNullableModule; public final class ModelConverter { @@ -30,10 +30,12 @@ public final class ModelConverter { private ModelConverter() {} public static T convert(final Response response, final Class clazz) { + final ResponseBody body = response.body(); try { - return mapper.readValue(readBodyAsString(response), clazz); + return mapper.readValue(body.string(), clazz); } catch (Exception e) { - throw buildConversionApiError(response, String.format("deserialize response body to %s", clazz.getSimpleName()), e); + e.printStackTrace(); + return null; } } @@ -48,9 +50,10 @@ public static T convert(final String response, final Class clazz) { public static T convert(Response response, TypeReference typeReference) { try { - return mapper.readValue(readBodyAsString(response), typeReference); + return convert(response.body().string(), typeReference); } catch (Exception e) { - throw buildConversionApiError(response, "deserialize response body", e); + e.printStackTrace(); + return null; } } @@ -65,9 +68,10 @@ public static T convert(String response, TypeReference typeReference) { public static String readString(Response response) { try { - return readBodyAsString(response); + return response.body().string(); } catch (Exception e) { - throw buildConversionApiError(response, "read response body as string", e); + e.printStackTrace(); + return null; } } @@ -78,7 +82,8 @@ public static byte[] readBytes(Response response) { } return response.body().bytes(); } catch (Exception e) { - throw buildConversionApiError(response, "read response body as bytes", e); + e.printStackTrace(); + return null; } } @@ -91,22 +96,4 @@ public static String modelToJson(final Object model) { } } - private static String readBodyAsString(Response response) throws Exception { - if (response.body() == null) { - throw new IllegalStateException("Response body is empty"); - } - - return response.body().string(); - } - - private static ApiError buildConversionApiError(Response response, String action, Exception cause) { - int status = response != null ? response.code() : 0; - String url = response != null && response.request() != null ? response.request().url().toString() : "unknown"; - - String message = String.format("Failed to %s (status: %d, url: %s)", action, status, url); - // TODO: Consider a dedicated response-conversion exception type for malformed 2xx payloads. - ApiError error = new ApiError(message, status, response); - error.initCause(cause); - return error; - } } diff --git a/src/main/java/com/asaas/apisdk/services/BaseService.java b/src/main/java/com/asaas/apisdk/services/BaseService.java index d1997e4..490d823 100644 --- a/src/main/java/com/asaas/apisdk/services/BaseService.java +++ b/src/main/java/com/asaas/apisdk/services/BaseService.java @@ -6,6 +6,8 @@ import com.asaas.apisdk.exceptions.ApiError; import com.asaas.apisdk.http.Environment; import com.asaas.apisdk.http.ModelConverter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.lang.reflect.Constructor; import java.net.SocketTimeoutException; @@ -80,10 +82,6 @@ private String extractErrorMessage(Response response, Object errorModel) { return message; } - private ApiError buildGenericApiError(Response response) { - return new ApiError(extractErrorMessage(response, null), response.code(), response); - } - protected Response execute(Request request) throws ApiError { Response response; try { @@ -130,7 +128,7 @@ protected Response execute(Request request) throws ApiError { } // If no specific error model is mapped or conversion failed, throw generic ApiError - throw buildGenericApiError(response); + throw new ApiError(extractErrorMessage(response, null), response.code(), response); } protected CompletableFuture executeAsync(Request request) { @@ -169,7 +167,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) { } // If no specific error model is mapped or conversion failed, throw generic ApiError - ApiError error = buildGenericApiError(response); + ApiError error = new ApiError(extractErrorMessage(response, null), response.code(), response); future.completeExceptionally(error); return; } From 0a7f1e80dcc4d60d87d5ea4508ddc0873ce579ee Mon Sep 17 00:00:00 2001 From: t31a Date: Thu, 9 Apr 2026 15:23:12 -0300 Subject: [PATCH 4/4] fix: remove leftover response conversion change --- src/main/java/com/asaas/apisdk/http/ModelConverter.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/asaas/apisdk/http/ModelConverter.java b/src/main/java/com/asaas/apisdk/http/ModelConverter.java index c2b138a..d74ff42 100644 --- a/src/main/java/com/asaas/apisdk/http/ModelConverter.java +++ b/src/main/java/com/asaas/apisdk/http/ModelConverter.java @@ -77,9 +77,6 @@ public static String readString(Response response) { public static byte[] readBytes(Response response) { try { - if (response.body() == null) { - throw new IllegalStateException("Response body is empty"); - } return response.body().bytes(); } catch (Exception e) { e.printStackTrace(); @@ -95,5 +92,4 @@ public static String modelToJson(final Object model) { return null; } } - }