Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,23 @@ private static ApiException wrapFailure(Throwable t, Response response) {
} catch (IOException e) {
log.debug("Failed to read SSE failure response body", e);
}
Status status =
Status.builder()
.statusCode(code)
.code(code >= 500 ? "server_error" : code == 401 ? "auth_error" : "http_error")
.message(body.isEmpty() ? "HTTP " + code : body)
.build();

// Try to extract original error code and message from response body
String apiCode = "";
String apiMessage = body.isEmpty() ? response.message() : body;
try {
com.google.gson.JsonObject json = com.alibaba.dashscope.utils.JsonUtils.parse(body);
if (json.has("code")) {
apiCode = json.get("code").getAsString();
}
if (json.has("message")) {
apiMessage = json.get("message").getAsString();
}
} catch (Exception e) {
log.debug("Failed to parse error response body as JSON", e);
}

Status status = Status.builder().statusCode(code).code(apiCode).message(apiMessage).build();
return new ApiException(status, t);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,10 @@ public void onResponse(Call call, Response response) {
if (!r.isSuccessful()) {
future.completeExceptionally(
new ApiException(
Status.builder().statusCode(r.code()).message(body).build()));
Status.builder()
.statusCode(r.code())
.message(body.isEmpty() ? r.message() : body)
.build()));
return;
}
future.complete(
Expand Down
71 changes: 66 additions & 5 deletions src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import com.alibaba.dashscope.base.HalfDuplexParamBase;
import com.alibaba.dashscope.common.DashScopeResult;
import com.alibaba.dashscope.common.PublicErrorDef;
import com.alibaba.dashscope.common.Status;
import com.alibaba.dashscope.common.TaskStatus;
import com.alibaba.dashscope.exception.ApiException;
Expand All @@ -18,8 +19,10 @@
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
Comment thread
luk384090-cloud marked this conversation as resolved.

/** Support DashScope async task CRUD. */
@Slf4j
public final class AsynchronousApi<ParamT extends HalfDuplexParamBase> {
final HalfDuplexClient client;
ConnectionOptions connectionOptions;
Expand Down Expand Up @@ -104,6 +107,10 @@ public DashScopeResult wait(
int maxWaitMilliseconds = 5 * 1000;
int incrementSteps = 3;
int step = 0;
int transientErrorCount = 0;
final int MAX_TRANSIENT_ERRORS = 20;
int transientBackoffMs = 1000;
final int MAX_TRANSIENT_BACKOFF_MS = 10000;
long startTime = System.currentTimeMillis();
long timeoutMillis = timeoutSeconds > 0 ? timeoutSeconds * 1000L : -1L;
while (true) {
Expand All @@ -112,12 +119,16 @@ public DashScopeResult wait(
if (elapsed >= timeoutMillis) {
throw new ApiException(
Status.builder()
.statusCode(HttpURLConnection.HTTP_CLIENT_TIMEOUT)
.code("TaskWaitTimeout")
.statusCode(PublicErrorDef.REQUEST_TIMEOUT.getStatusCode())
.code(PublicErrorDef.REQUEST_TIMEOUT.getErrorCode())
.message(
StringUtils.format(
"Waiting for task [%s] timed out after %d ms (timeoutSeconds=%d).",
taskId, elapsed, timeoutSeconds))
"%s [taskId=%s, elapsed=%d ms, timeoutSeconds=%d, transientErrors=%d]",
PublicErrorDef.REQUEST_TIMEOUT.getErrorMsg(),
taskId,
elapsed,
timeoutSeconds,
transientErrorCount))
.build());
}
}
Expand Down Expand Up @@ -155,14 +166,64 @@ public DashScopeResult wait(
}
try {
Thread.sleep(sleepMs);
} catch (InterruptedException ignored) {
} catch (InterruptedException e) {
Comment thread
luk384090-cloud marked this conversation as resolved.
Thread.currentThread().interrupt();
throw new ApiException(
Status.builder()
.statusCode(PublicErrorDef.INTERNAL_ERROR.getStatusCode())
.code(PublicErrorDef.INTERNAL_ERROR.getErrorCode())
.message(
StringUtils.format(
"%s [taskId=%s, reason=thread_interrupted]",
PublicErrorDef.INTERNAL_ERROR.getErrorMsg(), taskId))
.build(),
e);
}
}
} catch (ApiException e) {
if (e.getStatus().getStatusCode() != HttpURLConnection.HTTP_UNAVAILABLE
&& e.getStatus().getStatusCode() != HttpURLConnection.HTTP_GATEWAY_TIMEOUT) {
throw e;
}
transientErrorCount++;
Comment thread
luk384090-cloud marked this conversation as resolved.
log.warn(
"Transient error during async task polling [taskId={}]: status={}, message={}, retry_count={}, backoff_ms={}",
taskId,
e.getStatus().getStatusCode(),
e.getMessage(),
transientErrorCount,
transientBackoffMs);
if (transientErrorCount >= MAX_TRANSIENT_ERRORS) {
throw new ApiException(
Status.builder()
.statusCode(PublicErrorDef.SERVICE_UNAVAILABLE.getStatusCode())
.code(PublicErrorDef.SERVICE_UNAVAILABLE.getErrorCode())
.message(
StringUtils.format(
"%s [taskId=%s, transientErrors=%d, lastError=%s]",
PublicErrorDef.SERVICE_UNAVAILABLE.getErrorMsg(),
taskId,
transientErrorCount,
e.getMessage()))
.build());
}
transientBackoffMs = Math.min(transientBackoffMs * 2, MAX_TRANSIENT_BACKOFF_MS);
try {
Thread.sleep(transientBackoffMs);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new ApiException(
Status.builder()
.statusCode(PublicErrorDef.INTERNAL_ERROR.getStatusCode())
.code(PublicErrorDef.INTERNAL_ERROR.getErrorCode())
.message(
StringUtils.format(
"%s [taskId=%s, reason=thread_interrupted]",
PublicErrorDef.INTERNAL_ERROR.getErrorMsg(), taskId))
.build(),
ie);
}
Comment thread
luk384090-cloud marked this conversation as resolved.
continue;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ public void onError(Exception e) {
if (!completed) {
Status timeoutStatus =
Status.builder()
.statusCode(408)
.code("RequestTimeOut")
.message("Timeout waiting for audio data from server.")
.statusCode(PublicErrorDef.REQUEST_TIMEOUT.getStatusCode())
.code(PublicErrorDef.REQUEST_TIMEOUT.getErrorCode())
.message(PublicErrorDef.REQUEST_TIMEOUT.getErrorMsg())
.build();
throw new ApiException(timeoutStatus);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,16 +352,26 @@ private void handleTaskFinished(JsonObject message) {
private void handleTaskFailed(JsonObject message) {
log.error("Task failed: " + message.toString());
if (callback != null) {
String errorMessage = "Unknown error";
if (message.has("header") && message.getAsJsonObject("header").has("error_message")) {
errorMessage = message.getAsJsonObject("header").get("error_message").getAsString();
String errorCode = "";
String errorMessage = "";
int statusCode = -1;
if (message.has("header")) {
JsonObject header = message.getAsJsonObject("header");
if (header.has("error_code")) {
errorCode = header.get("error_code").getAsString();
}
if (header.has("error_message")) {
errorMessage = header.get("error_message").getAsString();
}
if (header.has("status_code") && !header.get("status_code").isJsonNull()) {
statusCode = header.get("status_code").getAsInt();
}
}

// Create a Status object for the ApiException
com.alibaba.dashscope.common.Status status =
com.alibaba.dashscope.common.Status.builder()
.statusCode(-1)
.code("TASK_FAILED")
.statusCode(statusCode)
.code(errorCode)
.message(errorMessage)
.build();
callback.onError(new ApiException(status));
Expand Down
107 changes: 104 additions & 3 deletions src/main/java/com/alibaba/dashscope/common/DashScopeResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import com.alibaba.dashscope.utils.ApiKeywords;
import com.alibaba.dashscope.utils.EncryptionUtils;
import com.alibaba.dashscope.utils.JsonUtils;
import com.alibaba.dashscope.utils.StringUtils;
import com.google.gson.JsonObject;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -68,6 +70,31 @@ protected <T extends Result> T fromResponse(Protocol protocol, NetworkResponse r
// Set default empty string for successful responses
this.setMessage("");
}
String errorCode = this.getCode();
if (errorCode != null && !errorCode.isEmpty()) {
int statusCode =
resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), errorCode);
throw new ApiException(
Status.builder()
.statusCode(statusCode > 0 ? statusCode : 500)
.code(errorCode)
.message(this.getMessage() != null ? this.getMessage() : "Unknown error")
.requestId(this.getRequestId())
.build());
} else if (response.getHttpStatusCode() >= 400) {
throw new ApiException(
Status.builder()
.statusCode(PublicErrorDef.INTERNAL_ERROR.getStatusCode())
.code(PublicErrorDef.INTERNAL_ERROR.getErrorCode())
.message(
StringUtils.format(
"%s [http_status=%d, original_message=%s]",
PublicErrorDef.INTERNAL_ERROR.getErrorMsg(),
response.getHttpStatusCode(),
(response.getMessage() != null ? response.getMessage() : "No message")))
.requestId(this.getRequestId())
.build());
}
}
if (jsonObject.has(ApiKeywords.PAYLOAD)) {
JsonObject payload = jsonObject.getAsJsonObject(ApiKeywords.PAYLOAD);
Expand Down Expand Up @@ -132,10 +159,19 @@ protected <T extends Result> T fromResponse(Protocol protocol, NetworkResponse r
// Set default empty string for successful responses
this.setMessage("");
}
if (this.getCode() != null && !this.getCode().isEmpty()) {
Comment thread
luk384090-cloud marked this conversation as resolved.
int resolvedStatusCode =
resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode());
throw new ApiException(
Status.builder()
.statusCode(resolvedStatusCode)
.code(this.getCode())
.message(this.getMessage())
.requestId(this.getRequestId())
.build());
}
Comment thread
luk384090-cloud marked this conversation as resolved.
if (jsonObject.has(ApiKeywords.DATA)) {
if (jsonObject.has(ApiKeywords.REQUEST_ID)) {
jsonObject.remove(ApiKeywords.REQUEST_ID);
}
jsonObject.remove(ApiKeywords.REQUEST_ID);
this.output = jsonObject;
}
}
Expand Down Expand Up @@ -230,6 +266,17 @@ public <T extends Result> T fromResponse(
// Set default empty string for successful responses
this.setMessage("");
}
if (this.getCode() != null && !this.getCode().isEmpty()) {
Comment thread
luk384090-cloud marked this conversation as resolved.
int resolvedStatusCode =
resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode());
throw new ApiException(
Status.builder()
.statusCode(resolvedStatusCode)
.code(this.getCode())
.message(this.getMessage())
.requestId(this.getRequestId())
.build());
}
Comment thread
luk384090-cloud marked this conversation as resolved.
if (jsonObject.has(ApiKeywords.DATA)) {
if (jsonObject.has(ApiKeywords.REQUEST_ID)) {
jsonObject.remove(ApiKeywords.REQUEST_ID);
Expand All @@ -256,4 +303,58 @@ private Map<String, Object> changeHeaders(Map<String, List<String>> headers) {
(v1, v2) -> v1,
java.util.LinkedHashMap::new));
}

/** Keyword-to-status mapping for legacy / non-standard error codes. */
private static final Map<String, Integer> LEGACY_ERROR_KEYWORDS = new LinkedHashMap<>();

static {
LEGACY_ERROR_KEYWORDS.put("InvalidParameter", 400);
LEGACY_ERROR_KEYWORDS.put("BadRequest", 400);
LEGACY_ERROR_KEYWORDS.put("Unauthorized", 401);
LEGACY_ERROR_KEYWORDS.put("ApiKey", 401);
LEGACY_ERROR_KEYWORDS.put("Forbidden", 403);
LEGACY_ERROR_KEYWORDS.put("AccessDenied", 403);
LEGACY_ERROR_KEYWORDS.put("NotFound", 404);
LEGACY_ERROR_KEYWORDS.put("Throttling", 429);
LEGACY_ERROR_KEYWORDS.put("RateLimit", 429);
LEGACY_ERROR_KEYWORDS.put("InternalError", 500);
LEGACY_ERROR_KEYWORDS.put("SystemError", 500);
}

/**
* Resolve the appropriate HTTP status code for an API exception. Priority: 1) Body status_code,
* 2) HTTP response status code, 3) Exact match in PublicErrorDef, 4) Keyword match for legacy
* error codes, 5) Default to bodyStatusCode/httpStatusCode/200.
*
* <p>This method is null-safe: all parameters accept {@code null} values and will never cause
* NullPointerException. Returns a primitive {@code int}, safe for direct use in builders.
*/
private int resolveStatusCode(Integer bodyStatusCode, Integer httpStatusCode, String errorCode) {
Comment thread
luk384090-cloud marked this conversation as resolved.
if (bodyStatusCode != null && bodyStatusCode != 200) {
return bodyStatusCode;
}
if (httpStatusCode != null && httpStatusCode != 200) {
return httpStatusCode;
}
if (errorCode != null) {
// Exact match against PublicErrorDef
for (PublicErrorDef def : PublicErrorDef.values()) {
if (def.getErrorCode().equals(errorCode)) {
return def.getStatusCode();
}
}
// Fallback: exact match first, then keyword match for non-standard / legacy error codes
if (LEGACY_ERROR_KEYWORDS.containsKey(errorCode)) {
return LEGACY_ERROR_KEYWORDS.get(errorCode);
}
for (Map.Entry<String, Integer> entry : LEGACY_ERROR_KEYWORDS.entrySet()) {
if (errorCode.contains(entry.getKey())) {
return entry.getValue();
}
}
}
return bodyStatusCode != null
? bodyStatusCode
: (httpStatusCode != null ? httpStatusCode : 200);
}
}
Loading
Loading