diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java index 8b75d6f7..b0084331 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java @@ -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); } diff --git a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java index 8397a0e6..f1348ed4 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/Files.java @@ -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( diff --git a/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java b/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java index c8302844..63b01d61 100644 --- a/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java +++ b/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java @@ -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; @@ -18,8 +19,10 @@ import java.net.HttpURLConnection; import java.util.HashMap; import java.util.Map; +import lombok.extern.slf4j.Slf4j; /** Support DashScope async task CRUD. */ +@Slf4j public final class AsynchronousApi { final HalfDuplexClient client; ConnectionOptions connectionOptions; @@ -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) { @@ -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()); } } @@ -155,7 +166,18 @@ public DashScopeResult wait( } try { Thread.sleep(sleepMs); - } catch (InterruptedException ignored) { + } catch (InterruptedException e) { + 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) { @@ -163,6 +185,45 @@ public DashScopeResult wait( && e.getStatus().getStatusCode() != HttpURLConnection.HTTP_GATEWAY_TIMEOUT) { throw e; } + transientErrorCount++; + 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); + } + continue; } } } diff --git a/src/main/java/com/alibaba/dashscope/audio/http_tts/HttpSpeechSynthesizer.java b/src/main/java/com/alibaba/dashscope/audio/http_tts/HttpSpeechSynthesizer.java index 2dde1ac7..6f6f02d1 100644 --- a/src/main/java/com/alibaba/dashscope/audio/http_tts/HttpSpeechSynthesizer.java +++ b/src/main/java/com/alibaba/dashscope/audio/http_tts/HttpSpeechSynthesizer.java @@ -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); } diff --git a/src/main/java/com/alibaba/dashscope/audio/ttsv2/SpeechSynthesizerV2.java b/src/main/java/com/alibaba/dashscope/audio/ttsv2/SpeechSynthesizerV2.java index 94acb336..99d59bc8 100644 --- a/src/main/java/com/alibaba/dashscope/audio/ttsv2/SpeechSynthesizerV2.java +++ b/src/main/java/com/alibaba/dashscope/audio/ttsv2/SpeechSynthesizerV2.java @@ -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)); diff --git a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java index ad71ae6d..b5b51b89 100644 --- a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java +++ b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java @@ -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; @@ -68,6 +70,31 @@ protected 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); @@ -132,10 +159,19 @@ protected T fromResponse(Protocol protocol, NetworkResponse r // Set default empty string for successful responses this.setMessage(""); } + if (this.getCode() != null && !this.getCode().isEmpty()) { + 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()); + } if (jsonObject.has(ApiKeywords.DATA)) { - if (jsonObject.has(ApiKeywords.REQUEST_ID)) { - jsonObject.remove(ApiKeywords.REQUEST_ID); - } + jsonObject.remove(ApiKeywords.REQUEST_ID); this.output = jsonObject; } } @@ -230,6 +266,17 @@ public T fromResponse( // Set default empty string for successful responses this.setMessage(""); } + if (this.getCode() != null && !this.getCode().isEmpty()) { + 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()); + } if (jsonObject.has(ApiKeywords.DATA)) { if (jsonObject.has(ApiKeywords.REQUEST_ID)) { jsonObject.remove(ApiKeywords.REQUEST_ID); @@ -256,4 +303,58 @@ private Map changeHeaders(Map> headers) { (v1, v2) -> v1, java.util.LinkedHashMap::new)); } + + /** Keyword-to-status mapping for legacy / non-standard error codes. */ + private static final Map 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. + * + *

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) { + 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 entry : LEGACY_ERROR_KEYWORDS.entrySet()) { + if (errorCode.contains(entry.getKey())) { + return entry.getValue(); + } + } + } + return bodyStatusCode != null + ? bodyStatusCode + : (httpStatusCode != null ? httpStatusCode : 200); + } } diff --git a/src/main/java/com/alibaba/dashscope/common/PublicErrorDef.java b/src/main/java/com/alibaba/dashscope/common/PublicErrorDef.java new file mode 100644 index 00000000..ea721eba --- /dev/null +++ b/src/main/java/com/alibaba/dashscope/common/PublicErrorDef.java @@ -0,0 +1,130 @@ +// Auto-generated by tools/generate.py -- DO NOT EDIT. +package com.alibaba.dashscope.common; + +import java.util.HashMap; +import java.util.Map; + +/** Customer-facing error definitions. */ +public enum PublicErrorDef { + INVALID_REQUEST( + 400, + "BadRequestError", + "The request is invalid. Please check the request and try again.", + "invalid_request_error"), + MISSING_PARAMETER( + 400, "BadRequestError", "Missing required parameter: {parameter}.", "invalid_request_error"), + CONTENT_POLICY_VIOLATION( + 400, + "BadRequestError", + "The request was rejected by content policy.", + "invalid_request_error"), + INVALID_URL( + 400, + "BadRequestError", + "The provided URL is invalid or cannot be accessed.", + "invalid_request_error"), + INVALID_FILE(400, "BadRequestError", "The provided file is invalid.", "invalid_request_error"), + AUTH_FAILED( + 401, + "AuthenticationError", + "Authentication failed. Please provide valid authentication credentials.", + "authentication_error"), + INVALID_API_KEY( + 401, "AuthenticationError", "Incorrect API key provided.", "authentication_error"), + PERMISSION_DENIED( + 403, + "PermissionDeniedError", + "You do not have permission to access this resource.", + "permission_error"), + RESOURCE_NOT_FOUND( + 404, "NotFoundError", "The requested resource was not found: {resource}.", "not_found_error"), + REQUEST_TOO_LARGE( + 413, + "RequestTooLargeError", + "The request exceeds the maximum allowed size of {limit}.", + "request_too_large"), + RATE_LIMIT_EXCEEDED( + 429, + "RateLimitError", + "Rate limit exceeded. Please slow down your requests.", + "rate_limit_error"), + CONCURRENCY_LIMIT_EXCEEDED( + 429, + "RateLimitError", + "Too many concurrent requests. Please reduce concurrency and try again.", + "rate_limit_error"), + INSUFFICIENT_QUOTA( + 429, + "RateLimitError", + "You exceeded your current quota. Please check your plan and billing details.", + "billing_error"), + INTERNAL_ERROR( + 500, + "InternalServerError", + "The server encountered an internal error. Please try again later.", + "api_error"), + SERVICE_UNAVAILABLE( + 503, + "ServiceUnavailableError", + "The service is temporarily unavailable. Please try again later.", + "overloaded_error"), + REQUEST_TIMEOUT( + 504, + "GatewayTimeoutError", + "The request timed out. Please try again later.", + "timeout_error"); + + private static final Map ERROR_CODE_MAP = new HashMap<>(); + + static { + for (PublicErrorDef def : values()) { + ERROR_CODE_MAP.put(def.errorCode, def); + } + } + + private final int statusCode; + private final String errorCode; + private final String errorMsg; + private final String anthropicErrorCode; + + PublicErrorDef(int statusCode, String errorCode, String errorMsg, String anthropicErrorCode) { + this.statusCode = statusCode; + this.errorCode = errorCode; + this.errorMsg = errorMsg; + this.anthropicErrorCode = anthropicErrorCode; + } + + public int getStatusCode() { + return statusCode; + } + + public String getErrorCode() { + return errorCode; + } + + public String getErrorMsg() { + return errorMsg; + } + + public String getAnthropicErrorCode() { + return anthropicErrorCode; + } + + public String formatMsg(Map vars) { + String msg = errorMsg; + for (Map.Entry entry : vars.entrySet()) { + msg = msg.replace("{" + entry.getKey() + "}", entry.getValue()); + } + return msg; + } + + /** + * Look up a PublicErrorDef by its error code. + * + * @param errorCode The error code to look up + * @return The matching PublicErrorDef, or null if not found + */ + public static PublicErrorDef fromErrorCode(String errorCode) { + return ERROR_CODE_MAP.get(errorCode); + } +} diff --git a/src/main/java/com/alibaba/dashscope/exception/ApiException.java b/src/main/java/com/alibaba/dashscope/exception/ApiException.java index 47321874..76051411 100644 --- a/src/main/java/com/alibaba/dashscope/exception/ApiException.java +++ b/src/main/java/com/alibaba/dashscope/exception/ApiException.java @@ -1,7 +1,7 @@ // Copyright (c) Alibaba, Inc. and its affiliates. package com.alibaba.dashscope.exception; -import com.alibaba.dashscope.common.ErrorType; +import com.alibaba.dashscope.common.PublicErrorDef; import com.alibaba.dashscope.common.Status; import com.alibaba.dashscope.utils.JsonUtils; import com.alibaba.dashscope.utils.StringUtils; @@ -19,9 +19,14 @@ public ApiException(Throwable e) { } else { this.status = Status.builder() - .statusCode(-1) - .code(ErrorType.NETWORK_ERROR.getValue()) - .message(StringUtils.format("%s: %s", e.getClass().getSimpleName(), e.getMessage())) + .statusCode(PublicErrorDef.SERVICE_UNAVAILABLE.getStatusCode()) + .code(PublicErrorDef.SERVICE_UNAVAILABLE.getErrorCode()) + .message( + StringUtils.format( + "%s [reason=wrapped_exception, detail=%s: %s]", + PublicErrorDef.SERVICE_UNAVAILABLE.getErrorMsg(), + e.getClass().getSimpleName(), + e.getMessage())) .build(); } this.setStackTrace(e.getStackTrace()); diff --git a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java index ff7fccb0..34e18786 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java @@ -4,7 +4,7 @@ import com.alibaba.dashscope.base.HalfDuplexParamBase; import com.alibaba.dashscope.common.DashScopeResult; -import com.alibaba.dashscope.common.ErrorType; +import com.alibaba.dashscope.common.PublicErrorDef; import com.alibaba.dashscope.common.ResultCallback; import com.alibaba.dashscope.common.Status; import com.alibaba.dashscope.exception.ApiException; @@ -53,7 +53,7 @@ public final class OkHttpHttpClient implements HalfDuplexClient { private static final MediaType MEDIA_TYPE_APPLICATION_JSON = MediaType.parse("application/json; charset=utf-8"); - private Status parseStreamEventData(String data) { + private Status parseStreamEventData(String data, int httpStatusCode) { try { JsonObject jsonResponse = JsonUtils.parse(data); String code = ""; @@ -62,14 +62,14 @@ private Status parseStreamEventData(String data) { if (jsonResponse.has(ApiKeywords.REQUEST_ID)) { requestId = jsonResponse.get(ApiKeywords.REQUEST_ID).getAsString(); } - if (jsonResponse.has(ApiKeywords.CODE)) { + if (jsonResponse.has(ApiKeywords.CODE) && !jsonResponse.get(ApiKeywords.CODE).isJsonNull()) { code = jsonResponse.get(ApiKeywords.CODE).getAsString(); } if (jsonResponse.has(ApiKeywords.MESSAGE)) { message = jsonResponse.get(ApiKeywords.MESSAGE).getAsString(); } return Status.builder() - .statusCode(400) + .statusCode(httpStatusCode) .code(code) .message(message) .requestId(requestId) @@ -77,8 +77,8 @@ private Status parseStreamEventData(String data) { .build(); } catch (Throwable e) { return Status.builder() - .statusCode(400) - .code(ErrorType.RESPONSE_ERROR.getValue()) + .statusCode(httpStatusCode) + .code("") .message(data) .isJson(false) .build(); @@ -94,7 +94,7 @@ private Status parseFailedJson(int statusCode, String body) { if (jsonResponse.has(ApiKeywords.REQUEST_ID)) { requestId = jsonResponse.get(ApiKeywords.REQUEST_ID).getAsString(); } - if (jsonResponse.has(ApiKeywords.CODE)) { + if (jsonResponse.has(ApiKeywords.CODE) && !jsonResponse.get(ApiKeywords.CODE).isJsonNull()) { code = jsonResponse.get(ApiKeywords.CODE).getAsString(); } if (jsonResponse.has(ApiKeywords.MESSAGE)) { @@ -108,11 +108,26 @@ private Status parseFailedJson(int statusCode, String body) { .isJson(true) .build(); } catch (Throwable e) { + // Try to extract code/message even if standard parsing failed + String extractedCode = ""; + String extractedMessage = body; + try { + JsonObject json = JsonUtils.parse(body); + if (json.has(ApiKeywords.CODE)) { + extractedCode = json.get(ApiKeywords.CODE).getAsString(); + } + if (json.has(ApiKeywords.MESSAGE)) { + extractedMessage = json.get(ApiKeywords.MESSAGE).getAsString(); + } + } catch (Exception ex) { + // Parsing failed, use defaults + } + return Status.builder() .statusCode(statusCode) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message(body) - .isJson(true) + .code(extractedCode.isEmpty() ? "" : extractedCode) + .message(extractedMessage) + .isJson(!extractedCode.isEmpty()) .build(); } } @@ -122,9 +137,13 @@ private Status parseFailed(Response response, Throwable th) { String message = th == null ? "Get response failed!" : th.getMessage(); return Status.builder() - .statusCode(-1) - .code(ErrorType.NETWORK_ERROR.getValue()) - .message(message) + .statusCode(PublicErrorDef.SERVICE_UNAVAILABLE.getStatusCode()) + .code(PublicErrorDef.SERVICE_UNAVAILABLE.getErrorCode()) + .message( + StringUtils.format( + "%s [reason=no_response, detail=%s]", + PublicErrorDef.SERVICE_UNAVAILABLE.getErrorMsg(), + (message != null ? message : "Unknown"))) .isJson(false) .build(); } @@ -136,10 +155,13 @@ private Status parseFailed(Response response, Throwable th) { body = response.body().string(); } catch (IOException e) { return Status.builder() - .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message("Failed read response body: " + e.getMessage()) - .isJson(true) + .statusCode(PublicErrorDef.INTERNAL_ERROR.getStatusCode()) + .code(PublicErrorDef.INTERNAL_ERROR.getErrorCode()) + .message( + StringUtils.format( + "%s [http_status=%d, reason=body_read_failed, detail=%s]", + PublicErrorDef.INTERNAL_ERROR.getErrorMsg(), response.code(), e.getMessage())) + .isJson(false) .build(); } return parseFailedJson(response.code(), body); @@ -154,25 +176,59 @@ private Status parseFailed(Response response, Throwable th) { } } return Status.builder() - .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message(body) + .statusCode(PublicErrorDef.INTERNAL_ERROR.getStatusCode()) + .code(PublicErrorDef.INTERNAL_ERROR.getErrorCode()) + .message( + StringUtils.format( + "%s [http_status=%d, content_type=text/event-stream, body=%s]", + PublicErrorDef.INTERNAL_ERROR.getErrorMsg(), + response.code(), + (body.isEmpty() ? response.message() : body))) .isJson(false) .build(); } catch (IOException e) { return Status.builder() - .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message("Failed read response body: " + e.getMessage()) - .isJson(true) + .statusCode(PublicErrorDef.INTERNAL_ERROR.getStatusCode()) + .code(PublicErrorDef.INTERNAL_ERROR.getErrorCode()) + .message( + StringUtils.format( + "%s [http_status=%d, reason=sse_body_read_failed, detail=%s]", + PublicErrorDef.INTERNAL_ERROR.getErrorMsg(), response.code(), e.getMessage())) + .isJson(false) .build(); } } else { + String body = ""; + try { + body = response.body().string(); + } catch (IOException e) { + log.debug("Failed to read non-JSON response body", e); + } + + // Try to extract code/message from body even if Content-Type is not application/json + String extractedCode = ""; + String extractedMessage = body.isEmpty() ? response.message() : body; + + try { + JsonObject json = JsonUtils.parse(body); + if (json.has(ApiKeywords.CODE)) { + extractedCode = json.get(ApiKeywords.CODE).getAsString(); + } + if (json.has(ApiKeywords.MESSAGE)) { + extractedMessage = json.get(ApiKeywords.MESSAGE).getAsString(); + } + } catch (Exception ex) { + // Parsing failed, use defaults + } + return Status.builder() .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message(response.message()) - .isJson(false) + .code( + extractedCode.isEmpty() + ? PublicErrorDef.INTERNAL_ERROR.getErrorCode() + : extractedCode) + .message(extractedMessage) + .isJson(!extractedCode.isEmpty()) .build(); } } @@ -255,6 +311,10 @@ public DashScopeResult send(HalfDuplexRequest req) throws NoApiKeyException, Api .build(), req.getIsFlatten(), req); + } catch (ApiException e) { + throw e; + } catch (NoApiKeyException e) { + throw e; } catch (Throwable e) { throw new ApiException(e); } @@ -308,7 +368,7 @@ private void handleSSEEvent( HalfDuplexRequest req) { log.debug(StringUtils.format("Event: id %s, type: %s, data: %s", id, eventType, data)); if (SSEEventType.ERROR.equals(eventType)) { - Status st = parseStreamEventData(data); + Status st = parseStreamEventData(data, response.code()); emitter.onError(new ApiException(st)); } else if (SSEEventType.DATA.equals(eventType) || SSEEventType.RESULT.equals(eventType)) { emitter.onNext( @@ -453,7 +513,7 @@ public void onEvent( java.lang.String data) { log.debug(StringUtils.format("Event: id %s, type: %s, data: %s", id, type, data)); if (SSEEventType.ERROR.equals(type)) { - Status st = parseStreamEventData(data); + Status st = parseStreamEventData(data, response.code()); callback.onError(new ApiException(st)); } else if (SSEEventType.DATA.equals(type) || SSEEventType.RESULT.equals(type)) { callback.onEvent( diff --git a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java index 333a0dfc..8f409f72 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java @@ -3,6 +3,7 @@ package com.alibaba.dashscope.protocol.okhttp; import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.PublicErrorDef; import com.alibaba.dashscope.common.ResultCallback; import com.alibaba.dashscope.common.Status; import com.alibaba.dashscope.exception.ApiException; @@ -176,9 +177,12 @@ private void establishWebSocketClient( } throw new ApiException( Status.builder() - .code("ConnectionError") - .message(errorMessage) - .statusCode(Constants.DASHSCOPE_WEBSOCKET_FAILED_STATUS_CODE) + .statusCode(PublicErrorDef.SERVICE_UNAVAILABLE.getStatusCode()) + .code(PublicErrorDef.SERVICE_UNAVAILABLE.getErrorCode()) + .message( + StringUtils.format( + "%s [originalError=%s]", + PublicErrorDef.SERVICE_UNAVAILABLE.getErrorMsg(), errorMessage)) .build()); } @@ -279,21 +283,35 @@ public void onMessage(WebSocket webSocket, String text) { case TASK_STARTED: // if has payload, call onNext. if (response.payload.output != null || response.payload.usage != null) { - responseEmitter.onNext( - new DashScopeResult() - .fromResponse( - Protocol.WEBSOCKET, - NetworkResponse.builder().message(text).build(), - isFlattenResult)); + try { + responseEmitter.onNext( + new DashScopeResult() + .fromResponse( + Protocol.WEBSOCKET, + NetworkResponse.builder().message(text).httpStatusCode(200).build(), + isFlattenResult)); + } catch (Exception e) { + log.error("Failed to create result for TASK_STARTED", e); + if (!responseEmitter.isCancelled()) { + responseEmitter.onError(e); + } + } } else if (passTaskStarted.get()) { - DashScopeResult start_message = - new DashScopeResult() - .fromResponse( - Protocol.WEBSOCKET, - NetworkResponse.builder().message(text).build(), - isFlattenResult); - start_message.setEvent(WebSocketEventType.TASK_STARTED.getValue()); - responseEmitter.onNext(start_message); + try { + DashScopeResult start_message = + new DashScopeResult() + .fromResponse( + Protocol.WEBSOCKET, + NetworkResponse.builder().message(text).httpStatusCode(200).build(), + isFlattenResult); + start_message.setEvent(WebSocketEventType.TASK_STARTED.getValue()); + responseEmitter.onNext(start_message); + } catch (Exception e) { + log.error("Failed to create start_message for TASK_STARTED", e); + if (!responseEmitter.isCancelled()) { + responseEmitter.onError(e); + } + } } break; case TASK_FAILED: @@ -312,31 +330,46 @@ public void onMessage(WebSocket webSocket, String text) { } else { log.error(StringUtils.format("Something wrong, receive task failed message: %s", text)); } + break; case TASK_FINISHED: // check the payload and usage is null. if (response.payload.output != null || response.payload.usage != null) { + try { + responseEmitter.onNext( + new DashScopeResult() + .fromResponse( + Protocol.WEBSOCKET, + NetworkResponse.builder().message(text).httpStatusCode(200).build(), + isFlattenResult)); + } catch (Exception e) { + log.error("[DEBUG] Failed to create result for TASK_FINISHED", e); + if (!responseEmitter.isCancelled()) { + responseEmitter.onError(e); + } + } + } + responseEmitter.onComplete(); + break; + case RESULT_GENERATED: + // get payload and usage. + try { responseEmitter.onNext( new DashScopeResult() .fromResponse( Protocol.WEBSOCKET, - NetworkResponse.builder().message(text).build(), + NetworkResponse.builder().message(text).httpStatusCode(200).build(), isFlattenResult)); + } catch (Exception e) { + log.error("Failed to create result for RESULT_GENERATED", e); + if (!responseEmitter.isCancelled()) { + responseEmitter.onError(e); + } } - responseEmitter.onComplete(); - break; - case RESULT_GENERATED: - // get payload and usage. - responseEmitter.onNext( - new DashScopeResult() - .fromResponse( - Protocol.WEBSOCKET, - NetworkResponse.builder().message(text).build(), - isFlattenResult)); break; default: - // throw new ApiException(Status.builder().code("") - // .message(StringUtils.format("Receive unknown message: %s", text)) - // .statusCode(Constants.DASHSCOPE_WEBSOCKET_FAILED_STATUS_CODE).build()); + // Protocol layer error: received undefined event type. + // This is SDK-level handling, not an API standard error code. + // Kept as hardcoded string to avoid polluting global ErrorType enum. responseEmitter.onError( new ApiException( Status.builder() @@ -346,6 +379,9 @@ public void onMessage(WebSocket webSocket, String text) { .build())); } } catch (Throwable ex) { + // Protocol layer error: JSON deserialization failed. + // This is SDK-level handling for malformed messages, not an API standard error. + // Kept as hardcoded string to maintain separation from business-layer errors. responseEmitter.onError( new ApiException( Status.builder() @@ -554,6 +590,8 @@ public DashScopeResult send(HalfDuplexRequest req) { public void send(HalfDuplexRequest req, ResultCallback callback) { if (req.getStreamingMode() == StreamingMode.NONE || req.getStreamingMode() == StreamingMode.IN) { + // Create flowable and subscribe with callback directly + // No need for the initial subscribe().dispose() pattern which causes emitter to be disposed Flowable flowable = Flowable.create( emitter -> { @@ -561,21 +599,28 @@ public void send(HalfDuplexRequest req, ResultCallback callback this.isFlattenResult = req.getIsFlatten(); }, BackpressureStrategy.BUFFER); - flowable.subscribe().dispose(); + + // Subscribe first to initialize responseEmitter + Disposable subscription = + flowable.subscribe( + msg -> { + callback.onEvent(msg); + }, + err -> { + callback.onError(new ApiException(err)); + }, + new Action() { + @Override + public void run() throws Exception { + callback.onComplete(); + } + }); + + // Now send the request - responseEmitter is already initialized and active sendBatchRequest(req); - flowable.subscribe( - msg -> { - callback.onEvent(msg); - }, - err -> { - callback.onError(new ApiException(err)); - }, - new Action() { - @Override - public void run() throws Exception { - callback.onComplete(); - } - }); + + // Note: Don't dispose here - let the WebSocket lifecycle manage the subscription + // The subscription will be completed when onClosing/onFailure is called } else { throw new ApiException( Status.builder() @@ -796,7 +841,8 @@ public Flowable duplex(FullDuplexRequest req) this.isFlattenResult = req.getIsFlatten(); }, BackpressureStrategy.BUFFER); - flowable.subscribe().dispose(); + // No need to subscribe here: sendStreamRequest() handles the actual WebSocket connection + // and the returned flowable will be subscribed by the caller CompletableFuture future = sendStreamRequest(req); return flowable diff --git a/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java b/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java index 80333e5d..cf71317b 100644 --- a/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java +++ b/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java @@ -188,7 +188,7 @@ public void onError(String errorMsg) { @Override public void onUnknown(String msg) { - System.out.println(msg); + // Unknown message type received, no action needed } @Override diff --git a/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java b/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java index 1ce094d6..9237e457 100644 --- a/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java +++ b/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java @@ -1,7 +1,7 @@ package com.alibaba.dashscope.utils; import com.alibaba.dashscope.common.DashScopeResult; -import com.alibaba.dashscope.common.ErrorType; +import com.alibaba.dashscope.common.PublicErrorDef; import com.alibaba.dashscope.common.Status; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.NoApiKeyException; @@ -33,6 +33,16 @@ @Slf4j public final class OSSUtils { + + /** Pre-built mapping from HTTP status code to PublicErrorDef for fast lookup. */ + private static final Map STATUS_CODE_TO_DEF = new HashMap<>(); + + static { + for (PublicErrorDef def : PublicErrorDef.values()) { + STATUS_CODE_TO_DEF.putIfAbsent(def.getStatusCode(), def); + } + } + /** * Upload file to OSS without certificate reuse. * @@ -189,18 +199,29 @@ private static Status parseFailed(Response response) { .isJson(isJson) .build(); } catch (Throwable e) { + PublicErrorDef matchedDef = STATUS_CODE_TO_DEF.get(response.code()); return Status.builder() .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message(response.message()) + .code(matchedDef != null ? matchedDef.getErrorCode() : "") + .message(matchedDef != null ? matchedDef.getErrorMsg() : response.message()) .isJson(isJson) .build(); } } else { + String body = ""; + try { + body = response.body().string(); + } catch (IOException e) { + log.debug("Failed to read non-JSON response body", e); + } + PublicErrorDef matchedDef = STATUS_CODE_TO_DEF.get(response.code()); return Status.builder() .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message(response.message()) + .code(matchedDef != null ? matchedDef.getErrorCode() : "") + .message( + matchedDef != null + ? matchedDef.getErrorMsg() + : (body.isEmpty() ? response.message() : body)) .isJson(isJson) .build(); } diff --git a/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java b/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java index 97da5f6c..a6e46944 100644 --- a/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java +++ b/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java @@ -151,7 +151,11 @@ public void onError(Exception e) { wsServer, JsonUtils.toJson(WebSocketServerMessage.getTaskGeneratedMessage(textOutput, usage))); wsServer.close(1000, "bye"); - semaphore.acquire(); + // Add timeout to avoid indefinite blocking due to race conditions + boolean acquired = semaphore.tryAcquire(5, TimeUnit.SECONDS); + if (!acquired) { + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); + } serverListener.assertHalfDuplexRequest(param, StreamingMode.NONE.getValue()); assertEquals(results.get(0).getOutput(), textOutput); assertEquals(results.get(0).getUsage(), usage); @@ -200,7 +204,11 @@ public void onError(Exception e) { wsServer, JsonUtils.toJson(WebSocketServerMessage.getTaskGeneratedMessage(textOutput, usage))); wsServer.close(1000, "bye"); - semaphore.acquire(); + // Add timeout to avoid indefinite blocking due to race conditions + boolean acquired = semaphore.tryAcquire(5, TimeUnit.SECONDS); + if (!acquired) { + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); + } serverListener.assertResources(resources); } @@ -242,7 +250,11 @@ public void onError(Exception e) {} wsServer, JsonUtils.toJson(WebSocketServerMessage.getTaskGeneratedMessage(textOutput, usage))); wsServer.close(1000, "bye"); - semaphore.acquire(); + // Add timeout to avoid indefinite blocking due to race conditions + boolean acquired = semaphore.tryAcquire(5, TimeUnit.SECONDS); + if (!acquired) { + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); + } serverListener.assertHalfDuplexRequest(param, StreamingMode.NONE.getValue()); assertEquals(results.get(0).getOutput(), textOutput); assertEquals(results.get(0).getUsage(), usage); @@ -286,7 +298,11 @@ public void onError(Exception e) {} sendTextWithRetry( wsServer, JsonUtils.toJson(WebSocketServerMessage.getTaskGeneratedMessage(null, usage))); wsServer.close(1000, "bye"); - semaphore.acquire(2); + // Add timeout to avoid indefinite blocking due to race conditions + boolean acquired = semaphore.tryAcquire(2, 5, TimeUnit.SECONDS); + if (!acquired) { + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); + } serverListener.assertHalfDuplexRequest(param, StreamingMode.NONE.getValue()); assertEquals((ByteBuffer) results.get(0).getOutput(), binaryOutput.position(0)); assertEquals(results.get(1).getUsage(), usage); @@ -330,7 +346,11 @@ public void onError(Exception e) {} sendTextWithRetry( wsServer, JsonUtils.toJson(WebSocketServerMessage.getTaskGeneratedMessage(null, usage))); wsServer.close(1000, "bye"); - semaphore.acquire(2); + // Add timeout to avoid indefinite blocking due to race conditions + boolean acquired = semaphore.tryAcquire(2, 5, TimeUnit.SECONDS); + if (!acquired) { + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); + } serverListener.assertHalfDuplexRequest(param, StreamingMode.NONE.getValue()); assertEquals((ByteBuffer) results.get(0).getOutput(), binaryOutput.position(0)); assertEquals(results.get(1).getUsage(), usage); @@ -377,7 +397,11 @@ public void onError(Exception e) {} sendTextWithRetry( wsServer, JsonUtils.toJson(WebSocketServerMessage.getTaskGeneratedMessage(null, usage))); wsServer.close(1000, "bye"); - semaphore.acquire(3); + // Add timeout to avoid indefinite blocking due to race conditions + boolean acquired = semaphore.tryAcquire(3, 5, TimeUnit.SECONDS); + if (!acquired) { + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); + } serverListener.assertHalfDuplexRequest(param, StreamingMode.NONE.getValue()); assertEquals((ByteBuffer) results.get(0).getOutput(), binaryOutput.position(0)); assertEquals(results.get(1).getOutput(), textOutput); diff --git a/src/test/java/com/alibaba/dashscope/TestHttpProxySettingWithCode.java b/src/test/java/com/alibaba/dashscope/TestHttpProxySettingWithCode.java index 8ed99450..0dfb3ffe 100644 --- a/src/test/java/com/alibaba/dashscope/TestHttpProxySettingWithCode.java +++ b/src/test/java/com/alibaba/dashscope/TestHttpProxySettingWithCode.java @@ -78,7 +78,9 @@ public void testSetProxyNoServer() throws NoApiKeyException { api.call(param, serviceOption); }); System.out.println(exception.getMessage()); - assertTrue(exception.getMessage().contains("network error")); + assertTrue( + exception.getMessage().contains("service is temporarily unavailable") + || exception.getMessage().contains("ServiceUnavailableError")); } @Test diff --git a/src/test/java/com/alibaba/dashscope/TestHttpTimeout.java b/src/test/java/com/alibaba/dashscope/TestHttpTimeout.java index c15535b4..0d3f4c91 100644 --- a/src/test/java/com/alibaba/dashscope/TestHttpTimeout.java +++ b/src/test/java/com/alibaba/dashscope/TestHttpTimeout.java @@ -72,7 +72,7 @@ public void testConnectionTimeout() throws ApiException, NoApiKeyException { }); long end = System.currentTimeMillis(); System.out.println(exception.getMessage()); - assertTrue(exception.getMessage().contains("unknown_error")); + assertTrue(exception.getMessage().contains("network error")); assertTrue(end - start > timeoutSeconds * 1000); } @@ -102,7 +102,7 @@ public void testReadTimeout() throws ApiException, NoApiKeyException { }); long end = System.currentTimeMillis(); System.out.println(exception.getMessage()); - assertTrue(exception.getMessage().contains("unknown_error")); + assertTrue(exception.getMessage().contains("body_read_error")); assertTrue(end - start > timeoutSeconds * 1000); } diff --git a/src/test/java/com/alibaba/dashscope/TestTtsV2SpeechSynthesizer.java b/src/test/java/com/alibaba/dashscope/TestTtsV2SpeechSynthesizer.java index c6858cf3..4f5e8561 100644 --- a/src/test/java/com/alibaba/dashscope/TestTtsV2SpeechSynthesizer.java +++ b/src/test/java/com/alibaba/dashscope/TestTtsV2SpeechSynthesizer.java @@ -8,7 +8,7 @@ import com.alibaba.dashscope.audio.ttsv2.ParamHotFix; import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisAudioFormat; import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam; -import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer; +import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizerV2; import com.alibaba.dashscope.common.ResultCallback; import com.alibaba.dashscope.utils.Constants; import com.alibaba.dashscope.utils.JsonUtils; @@ -151,9 +151,9 @@ public void testStreamingCall() { .format(SpeechSynthesisAudioFormat.MP3_16000HZ_MONO_128KBPS) .hotFix(hotFix) .build(); - SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, callback); - synthesizer.setStartedTimeout(1000); - synthesizer.setFirstAudioTimeout(2000); + SpeechSynthesizerV2 synthesizer = new SpeechSynthesizerV2(param, callback); + synthesizer.setStartedTimeout(10000); + synthesizer.setFirstAudioTimeout(15000); for (int i = 0; i < 3; i++) { synthesizer.streamingCall("今天天气怎么样?"); }