From 87fe2a5625da70515b43e4471027dae4843ecc1c Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 15 Jul 2026 16:17:39 +0800 Subject: [PATCH 1/8] fix: standardize error handling and improve resilience across SDK --- .../resource/AgentStudioEventStream.java | 23 ++++-- .../dashscope/api/AsynchronousApi.java | 45 ++++++++++- .../audio/http_tts/HttpSpeechSynthesizer.java | 2 +- .../audio/ttsv2/SpeechSynthesizerV2.java | 16 ++-- .../dashscope/common/DashScopeResult.java | 27 +++++++ .../alibaba/dashscope/common/ErrorType.java | 26 +++--- .../protocol/okhttp/OkHttpHttpClient.java | 80 ++++++++++++++----- .../okhttp/OkHttpWebSocketClient.java | 13 ++- .../runs/DefaultAssistantEventHandler.java | 4 +- .../com/alibaba/dashscope/utils/OSSUtils.java | 13 ++- 10 files changed, 195 insertions(+), 54 deletions(-) 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..f7a194fc 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; + 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/api/AsynchronousApi.java b/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java index c8302844..c842d83a 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.ErrorType; 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) { @@ -113,11 +120,11 @@ public DashScopeResult wait( throw new ApiException( Status.builder() .statusCode(HttpURLConnection.HTTP_CLIENT_TIMEOUT) - .code("TaskWaitTimeout") + .code(ErrorType.TASK_WAIT_TIMEOUT.getValue()) .message( StringUtils.format( - "Waiting for task [%s] timed out after %d ms (timeoutSeconds=%d).", - taskId, elapsed, timeoutSeconds)) + "Waiting for task [%s] timed out after %d ms (timeoutSeconds=%d). Encountered %d transient errors (503/504) during polling.", + taskId, elapsed, timeoutSeconds, transientErrorCount)) .build()); } } @@ -163,6 +170,38 @@ 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(e.getStatus().getStatusCode()) + .code("TooManyTransientErrors") + .message( + StringUtils.format( + "Encountered %d transient errors (503/504) while waiting for task [%s]. The service may be experiencing issues. Last error: %s", + transientErrorCount, taskId, e.getMessage())) + .build()); + } + long sleepMs = transientBackoffMs; + if (timeoutMillis > 0) { + long remaining = timeoutMillis - (System.currentTimeMillis() - startTime); + if (remaining <= 0) { + continue; + } + sleepMs = Math.min(sleepMs, remaining); + } + try { + Thread.sleep(sleepMs); + } catch (InterruptedException ignored) { + } + transientBackoffMs = Math.min(transientBackoffMs * 2, MAX_TRANSIENT_BACKOFF_MS); } } } 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..1106d602 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 @@ -201,7 +201,7 @@ public void onError(Exception e) { Status timeoutStatus = Status.builder() .statusCode(408) - .code("RequestTimeOut") + .code(ErrorType.REQUEST_TIMEOUT.getValue()) .message("Timeout waiting for audio data from server.") .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..3b9878f0 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,22 @@ 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 = ""; + 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(); + } } - // Create a Status object for the ApiException com.alibaba.dashscope.common.Status status = com.alibaba.dashscope.common.Status.builder() .statusCode(-1) - .code("TASK_FAILED") + .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..a4a0f7ba 100644 --- a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java +++ b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java @@ -68,6 +68,15 @@ protected T fromResponse(Protocol protocol, NetworkResponse r // Set default empty string for successful responses this.setMessage(""); } + if (this.getCode() != null && !this.getCode().isEmpty()) { + throw new ApiException( + Status.builder() + .statusCode(this.getStatusCode()) + .code(this.getCode()) + .message(this.getMessage()) + .requestId(this.getRequestId()) + .build()); + } } if (jsonObject.has(ApiKeywords.PAYLOAD)) { JsonObject payload = jsonObject.getAsJsonObject(ApiKeywords.PAYLOAD); @@ -132,6 +141,15 @@ protected T fromResponse(Protocol protocol, NetworkResponse r // Set default empty string for successful responses this.setMessage(""); } + if (this.getCode() != null && !this.getCode().isEmpty()) { + throw new ApiException( + Status.builder() + .statusCode(this.getStatusCode()) + .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); @@ -230,6 +248,15 @@ public T fromResponse( // Set default empty string for successful responses this.setMessage(""); } + if (this.getCode() != null && !this.getCode().isEmpty()) { + throw new ApiException( + Status.builder() + .statusCode(this.getStatusCode()) + .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); diff --git a/src/main/java/com/alibaba/dashscope/common/ErrorType.java b/src/main/java/com/alibaba/dashscope/common/ErrorType.java index 85dc4ec8..090df407 100644 --- a/src/main/java/com/alibaba/dashscope/common/ErrorType.java +++ b/src/main/java/com/alibaba/dashscope/common/ErrorType.java @@ -3,22 +3,26 @@ public enum ErrorType { - /** The error happens in the response body. */ - RESPONSE_ERROR("response_error"), + /** Network error: DNS failure, connection refused, etc. API did not respond. */ + NETWORK_ERROR("network error"), - /** The error happens because the request is canceled. */ - REQUEST_CANCELLED("request_cancelled"), + /** WebSocket connection failed after retries. API returned no content. */ + CONNECTION_ERROR("ConnectionError"), - /** The error happens because the network protocol is not supported. */ - PROTOCOL_UNSUPPORTED("protocol_unsupported"), + /** Asynchronous task polling timed out. API is still processing. */ + TASK_WAIT_TIMEOUT("TaskWaitTimeout"), - /** The api key is not correct. */ - API_KEY_ERROR("api_key_error"), + /** HTTP TTS waiting for audio data timed out. API returned no error. */ + REQUEST_TIMEOUT("RequestTimeOut"), - /** An unknown error. */ - UNKNOWN_ERROR("unknown_error"), + /** JSON parsing failed. Original body preserved in message field. */ + JSON_PARSE_ERROR("json_parse_error"), - NETWORK_ERROR("network error"), + /** Failed to read response body due to IOException. HTTP status code preserved. */ + BODY_READ_ERROR("body_read_error"), + + /** Non-JSON content type received (e.g., HTML error page). Original body in message. */ + NON_JSON_RESPONSE("non_json_response"), ; private final String value; 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..61a508cd 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java @@ -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 = ""; @@ -69,7 +69,7 @@ private Status parseStreamEventData(String data) { 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(); @@ -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(); } } @@ -137,9 +152,9 @@ private Status parseFailed(Response response, Throwable th) { } catch (IOException e) { return Status.builder() .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message("Failed read response body: " + e.getMessage()) - .isJson(true) + .code("") + .message("[SDK] Failed to read response body: " + e.getMessage()) + .isJson(false) .build(); } return parseFailedJson(response.code(), body); @@ -155,24 +170,47 @@ private Status parseFailed(Response response, Throwable th) { } return Status.builder() .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) + .code("") .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) + .code("") + .message("[SDK] Failed to read SSE response body: " + 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() ? "" : extractedCode) + .message(extractedMessage) + .isJson(!extractedCode.isEmpty()) .build(); } } @@ -255,6 +293,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 +350,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 +495,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..f257fe8d 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.ErrorType; import com.alibaba.dashscope.common.ResultCallback; import com.alibaba.dashscope.common.Status; import com.alibaba.dashscope.exception.ApiException; @@ -176,7 +177,7 @@ private void establishWebSocketClient( } throw new ApiException( Status.builder() - .code("ConnectionError") + .code(ErrorType.CONNECTION_ERROR.getValue()) .message(errorMessage) .statusCode(Constants.DASHSCOPE_WEBSOCKET_FAILED_STATUS_CODE) .build()); @@ -312,6 +313,7 @@ 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) { @@ -334,9 +336,9 @@ public void onMessage(WebSocket webSocket, String text) { 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 +348,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() 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..8562c539 100644 --- a/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java +++ b/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java @@ -6,8 +6,10 @@ import io.reactivex.Flowable; import io.reactivex.FlowableEmitter; import lombok.Data; +import lombok.extern.slf4j.Slf4j; /** An sample assistant event handler. */ +@Slf4j @Data public class DefaultAssistantEventHandler implements AssistantEventHandler { private AssistantThread assistantThread; @@ -188,7 +190,7 @@ public void onError(String errorMsg) { @Override public void onUnknown(String msg) { - System.out.println(msg); + log.warn("Received unknown message: {}", msg); } @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..b806d141 100644 --- a/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java +++ b/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java @@ -1,7 +1,6 @@ package com.alibaba.dashscope.utils; import com.alibaba.dashscope.common.DashScopeResult; -import com.alibaba.dashscope.common.ErrorType; import com.alibaba.dashscope.common.Status; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.NoApiKeyException; @@ -191,16 +190,22 @@ private static Status parseFailed(Response response) { } catch (Throwable e) { return Status.builder() .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) + .code("") .message(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); + } return Status.builder() .statusCode(response.code()) - .code(ErrorType.RESPONSE_ERROR.getValue()) - .message(response.message()) + .code("") + .message(body.isEmpty() ? response.message() : body) .isJson(isJson) .build(); } From 4054ef501a802bf6cb5c2d6b89cd22b756713970 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 15 Jul 2026 16:35:36 +0800 Subject: [PATCH 2/8] fix: prevent NPE, empty error messages, and swallowed InterruptedException --- .../resource/AgentStudioEventStream.java | 2 +- .../dashscope/agentstudio/resource/Files.java | 5 ++++- .../dashscope/api/AsynchronousApi.java | 20 +++++++++++++++++-- .../dashscope/common/DashScopeResult.java | 6 +++--- .../protocol/okhttp/OkHttpHttpClient.java | 2 +- 5 files changed, 27 insertions(+), 8 deletions(-) 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 f7a194fc..b0084331 100644 --- a/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java +++ b/src/main/java/com/alibaba/dashscope/agentstudio/resource/AgentStudioEventStream.java @@ -98,7 +98,7 @@ private static ApiException wrapFailure(Throwable t, Response response) { // Try to extract original error code and message from response body String apiCode = ""; - String apiMessage = body; + String apiMessage = body.isEmpty() ? response.message() : body; try { com.google.gson.JsonObject json = com.alibaba.dashscope.utils.JsonUtils.parse(body); if (json.has("code")) { 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 c842d83a..60c6745e 100644 --- a/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java +++ b/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java @@ -162,7 +162,15 @@ public DashScopeResult wait( } try { Thread.sleep(sleepMs); - } catch (InterruptedException ignored) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ApiException( + Status.builder() + .statusCode(-1) + .code("Interrupted") + .message("Thread was interrupted while waiting for task.") + .build(), + e); } } } catch (ApiException e) { @@ -199,7 +207,15 @@ public DashScopeResult wait( } try { Thread.sleep(sleepMs); - } catch (InterruptedException ignored) { + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new ApiException( + Status.builder() + .statusCode(-1) + .code("Interrupted") + .message("Thread was interrupted while waiting for task.") + .build(), + ie); } transientBackoffMs = Math.min(transientBackoffMs * 2, MAX_TRANSIENT_BACKOFF_MS); } diff --git a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java index a4a0f7ba..6894c93d 100644 --- a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java +++ b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java @@ -71,7 +71,7 @@ protected T fromResponse(Protocol protocol, NetworkResponse r if (this.getCode() != null && !this.getCode().isEmpty()) { throw new ApiException( Status.builder() - .statusCode(this.getStatusCode()) + .statusCode(this.getStatusCode() == null ? 200 : this.getStatusCode()) .code(this.getCode()) .message(this.getMessage()) .requestId(this.getRequestId()) @@ -144,7 +144,7 @@ protected T fromResponse(Protocol protocol, NetworkResponse r if (this.getCode() != null && !this.getCode().isEmpty()) { throw new ApiException( Status.builder() - .statusCode(this.getStatusCode()) + .statusCode(this.getStatusCode() == null ? 200 : this.getStatusCode()) .code(this.getCode()) .message(this.getMessage()) .requestId(this.getRequestId()) @@ -251,7 +251,7 @@ public T fromResponse( if (this.getCode() != null && !this.getCode().isEmpty()) { throw new ApiException( Status.builder() - .statusCode(this.getStatusCode()) + .statusCode(this.getStatusCode() == null ? 200 : this.getStatusCode()) .code(this.getCode()) .message(this.getMessage()) .requestId(this.getRequestId()) 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 61a508cd..8f656eeb 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java @@ -171,7 +171,7 @@ private Status parseFailed(Response response, Throwable th) { return Status.builder() .statusCode(response.code()) .code("") - .message(body) + .message(body.isEmpty() ? response.message() : body) .isJson(false) .build(); } catch (IOException e) { From 8952eb987189976eda1d283afdcdaef2d3e61903 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 16 Jul 2026 09:59:15 +0800 Subject: [PATCH 3/8] fix: resolve incorrect statusCode when HTTP 200 with body error --- .../dashscope/common/DashScopeResult.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java index 6894c93d..ab53daf3 100644 --- a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java +++ b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java @@ -69,9 +69,10 @@ protected T fromResponse(Protocol protocol, NetworkResponse r this.setMessage(""); } if (this.getCode() != null && !this.getCode().isEmpty()) { + int resolvedStatusCode = resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); throw new ApiException( Status.builder() - .statusCode(this.getStatusCode() == null ? 200 : this.getStatusCode()) + .statusCode(resolvedStatusCode) .code(this.getCode()) .message(this.getMessage()) .requestId(this.getRequestId()) @@ -142,9 +143,10 @@ protected T fromResponse(Protocol protocol, NetworkResponse r this.setMessage(""); } if (this.getCode() != null && !this.getCode().isEmpty()) { + int resolvedStatusCode = resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); throw new ApiException( Status.builder() - .statusCode(this.getStatusCode() == null ? 200 : this.getStatusCode()) + .statusCode(resolvedStatusCode) .code(this.getCode()) .message(this.getMessage()) .requestId(this.getRequestId()) @@ -249,9 +251,10 @@ public T fromResponse( this.setMessage(""); } if (this.getCode() != null && !this.getCode().isEmpty()) { + int resolvedStatusCode = resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); throw new ApiException( Status.builder() - .statusCode(this.getStatusCode() == null ? 200 : this.getStatusCode()) + .statusCode(resolvedStatusCode) .code(this.getCode()) .message(this.getMessage()) .requestId(this.getRequestId()) @@ -283,4 +286,34 @@ private Map changeHeaders(Map> headers) { (v1, v2) -> v1, java.util.LinkedHashMap::new)); } + + /** + * Resolve the appropriate HTTP status code for an API exception. + * Priority: 1) Body status_code, 2) HTTP response status code, 3) Infer from error code. + */ + private int resolveStatusCode(Integer bodyStatusCode, Integer httpStatusCode, String errorCode) { + if (bodyStatusCode != null && bodyStatusCode != 200) { + return bodyStatusCode; + } + if (httpStatusCode != null && httpStatusCode != 200) { + return httpStatusCode; + } + // Infer status code from business error code when both are 200 or null + if (errorCode != null) { + if (errorCode.contains("InvalidParameter") || errorCode.contains("BadRequest")) { + return 400; + } else if (errorCode.contains("Unauthorized") || errorCode.contains("ApiKey")) { + return 401; + } else if (errorCode.contains("Forbidden") || errorCode.contains("AccessDenied")) { + return 403; + } else if (errorCode.contains("NotFound")) { + return 404; + } else if (errorCode.contains("Throttling") || errorCode.contains("RateLimit")) { + return 429; + } else if (errorCode.contains("InternalError") || errorCode.contains("SystemError")) { + return 500; + } + } + return bodyStatusCode != null ? bodyStatusCode : (httpStatusCode != null ? httpStatusCode : 200); + } } From 0640a05892da3d77aa04bcb6bfbe60621242b99b Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 16 Jul 2026 10:02:36 +0800 Subject: [PATCH 4/8] fix: resolve incorrect statusCode when HTTP 200 with body error --- .../dashscope/common/DashScopeResult.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java index ab53daf3..88d01ab2 100644 --- a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java +++ b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java @@ -69,7 +69,9 @@ protected T fromResponse(Protocol protocol, NetworkResponse r this.setMessage(""); } if (this.getCode() != null && !this.getCode().isEmpty()) { - int resolvedStatusCode = resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); + int resolvedStatusCode = + resolveStatusCode( + this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); throw new ApiException( Status.builder() .statusCode(resolvedStatusCode) @@ -143,7 +145,8 @@ protected T fromResponse(Protocol protocol, NetworkResponse r this.setMessage(""); } if (this.getCode() != null && !this.getCode().isEmpty()) { - int resolvedStatusCode = resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); + int resolvedStatusCode = + resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); throw new ApiException( Status.builder() .statusCode(resolvedStatusCode) @@ -251,7 +254,8 @@ public T fromResponse( this.setMessage(""); } if (this.getCode() != null && !this.getCode().isEmpty()) { - int resolvedStatusCode = resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); + int resolvedStatusCode = + resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), this.getCode()); throw new ApiException( Status.builder() .statusCode(resolvedStatusCode) @@ -288,8 +292,8 @@ private Map changeHeaders(Map> headers) { } /** - * Resolve the appropriate HTTP status code for an API exception. - * Priority: 1) Body status_code, 2) HTTP response status code, 3) Infer from error code. + * Resolve the appropriate HTTP status code for an API exception. Priority: 1) Body status_code, + * 2) HTTP response status code, 3) Infer from error code. */ private int resolveStatusCode(Integer bodyStatusCode, Integer httpStatusCode, String errorCode) { if (bodyStatusCode != null && bodyStatusCode != 200) { @@ -314,6 +318,8 @@ private int resolveStatusCode(Integer bodyStatusCode, Integer httpStatusCode, St return 500; } } - return bodyStatusCode != null ? bodyStatusCode : (httpStatusCode != null ? httpStatusCode : 200); + return bodyStatusCode != null + ? bodyStatusCode + : (httpStatusCode != null ? httpStatusCode : 200); } } From f8a4bb8c2cc9088798c78a282bee83f95fb96111 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 21 Jul 2026 15:19:29 +0800 Subject: [PATCH 5/8] refactor: standardize error handling with PublicErrorDef --- .../dashscope/api/AsynchronousApi.java | 56 ++++---- .../audio/ttsv2/SpeechSynthesizerV2.java | 6 +- .../dashscope/common/DashScopeResult.java | 83 +++++++---- .../dashscope/common/PublicErrorDef.java | 130 ++++++++++++++++++ .../protocol/okhttp/OkHttpHttpClient.java | 12 +- .../okhttp/OkHttpWebSocketClient.java | 14 +- .../runs/DefaultAssistantEventHandler.java | 4 +- .../com/alibaba/dashscope/utils/OSSUtils.java | 24 +++- .../alibaba/dashscope/TestHttpTimeout.java | 4 +- .../dashscope/TestTtsV2SpeechSynthesizer.java | 8 +- 10 files changed, 267 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/alibaba/dashscope/common/PublicErrorDef.java diff --git a/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java b/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java index 60c6745e..63b01d61 100644 --- a/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java +++ b/src/main/java/com/alibaba/dashscope/api/AsynchronousApi.java @@ -6,7 +6,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.Status; import com.alibaba.dashscope.common.TaskStatus; import com.alibaba.dashscope.exception.ApiException; @@ -119,12 +119,16 @@ public DashScopeResult wait( if (elapsed >= timeoutMillis) { throw new ApiException( Status.builder() - .statusCode(HttpURLConnection.HTTP_CLIENT_TIMEOUT) - .code(ErrorType.TASK_WAIT_TIMEOUT.getValue()) + .statusCode(PublicErrorDef.REQUEST_TIMEOUT.getStatusCode()) + .code(PublicErrorDef.REQUEST_TIMEOUT.getErrorCode()) .message( StringUtils.format( - "Waiting for task [%s] timed out after %d ms (timeoutSeconds=%d). Encountered %d transient errors (503/504) during polling.", - taskId, elapsed, timeoutSeconds, transientErrorCount)) + "%s [taskId=%s, elapsed=%d ms, timeoutSeconds=%d, transientErrors=%d]", + PublicErrorDef.REQUEST_TIMEOUT.getErrorMsg(), + taskId, + elapsed, + timeoutSeconds, + transientErrorCount)) .build()); } } @@ -166,9 +170,12 @@ public DashScopeResult wait( Thread.currentThread().interrupt(); throw new ApiException( Status.builder() - .statusCode(-1) - .code("Interrupted") - .message("Thread was interrupted while waiting for task.") + .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); } @@ -189,35 +196,34 @@ public DashScopeResult wait( if (transientErrorCount >= MAX_TRANSIENT_ERRORS) { throw new ApiException( Status.builder() - .statusCode(e.getStatus().getStatusCode()) - .code("TooManyTransientErrors") + .statusCode(PublicErrorDef.SERVICE_UNAVAILABLE.getStatusCode()) + .code(PublicErrorDef.SERVICE_UNAVAILABLE.getErrorCode()) .message( StringUtils.format( - "Encountered %d transient errors (503/504) while waiting for task [%s]. The service may be experiencing issues. Last error: %s", - transientErrorCount, taskId, e.getMessage())) + "%s [taskId=%s, transientErrors=%d, lastError=%s]", + PublicErrorDef.SERVICE_UNAVAILABLE.getErrorMsg(), + taskId, + transientErrorCount, + e.getMessage())) .build()); } - long sleepMs = transientBackoffMs; - if (timeoutMillis > 0) { - long remaining = timeoutMillis - (System.currentTimeMillis() - startTime); - if (remaining <= 0) { - continue; - } - sleepMs = Math.min(sleepMs, remaining); - } + transientBackoffMs = Math.min(transientBackoffMs * 2, MAX_TRANSIENT_BACKOFF_MS); try { - Thread.sleep(sleepMs); + Thread.sleep(transientBackoffMs); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new ApiException( Status.builder() - .statusCode(-1) - .code("Interrupted") - .message("Thread was interrupted while waiting for task.") + .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); } - transientBackoffMs = Math.min(transientBackoffMs * 2, MAX_TRANSIENT_BACKOFF_MS); + continue; } } } 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 3b9878f0..99d59bc8 100644 --- a/src/main/java/com/alibaba/dashscope/audio/ttsv2/SpeechSynthesizerV2.java +++ b/src/main/java/com/alibaba/dashscope/audio/ttsv2/SpeechSynthesizerV2.java @@ -354,6 +354,7 @@ private void handleTaskFailed(JsonObject message) { if (callback != null) { String errorCode = ""; String errorMessage = ""; + int statusCode = -1; if (message.has("header")) { JsonObject header = message.getAsJsonObject("header"); if (header.has("error_code")) { @@ -362,11 +363,14 @@ private void handleTaskFailed(JsonObject message) { 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(); + } } com.alibaba.dashscope.common.Status status = com.alibaba.dashscope.common.Status.builder() - .statusCode(-1) + .statusCode(statusCode) .code(errorCode) .message(errorMessage) .build(); diff --git a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java index 88d01ab2..786f8587 100644 --- a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java +++ b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java @@ -11,6 +11,7 @@ import com.alibaba.dashscope.utils.JsonUtils; 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,15 +69,29 @@ 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()); + String errorCode = this.getCode(); + if (errorCode != null && !errorCode.isEmpty()) { + int statusCode = + resolveStatusCode(this.getStatusCode(), response.getHttpStatusCode(), errorCode); throw new ApiException( Status.builder() - .statusCode(resolvedStatusCode) - .code(this.getCode()) - .message(this.getMessage()) + .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(response.getHttpStatusCode()) + .code(ErrorType.NON_JSON_RESPONSE.getValue()) + .message( + "HTTP " + + response.getHttpStatusCode() + + ": " + + (response.getMessage() != null + ? response.getMessage() + : "No message")) .requestId(this.getRequestId()) .build()); } @@ -156,9 +171,7 @@ protected T fromResponse(Protocol protocol, NetworkResponse r .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; } } @@ -291,9 +304,30 @@ private Map changeHeaders(Map> headers) { 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) Infer from error 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) { @@ -302,20 +336,21 @@ private int resolveStatusCode(Integer bodyStatusCode, Integer httpStatusCode, St if (httpStatusCode != null && httpStatusCode != 200) { return httpStatusCode; } - // Infer status code from business error code when both are 200 or null if (errorCode != null) { - if (errorCode.contains("InvalidParameter") || errorCode.contains("BadRequest")) { - return 400; - } else if (errorCode.contains("Unauthorized") || errorCode.contains("ApiKey")) { - return 401; - } else if (errorCode.contains("Forbidden") || errorCode.contains("AccessDenied")) { - return 403; - } else if (errorCode.contains("NotFound")) { - return 404; - } else if (errorCode.contains("Throttling") || errorCode.contains("RateLimit")) { - return 429; - } else if (errorCode.contains("InternalError") || errorCode.contains("SystemError")) { - return 500; + // 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 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/protocol/okhttp/OkHttpHttpClient.java b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java index 8f656eeb..fbd857ad 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpHttpClient.java @@ -62,7 +62,7 @@ private Status parseStreamEventData(String data, int httpStatusCode) { 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)) { @@ -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)) { @@ -152,7 +152,7 @@ private Status parseFailed(Response response, Throwable th) { } catch (IOException e) { return Status.builder() .statusCode(response.code()) - .code("") + .code(ErrorType.BODY_READ_ERROR.getValue()) .message("[SDK] Failed to read response body: " + e.getMessage()) .isJson(false) .build(); @@ -170,14 +170,14 @@ private Status parseFailed(Response response, Throwable th) { } return Status.builder() .statusCode(response.code()) - .code("") + .code(ErrorType.NON_JSON_RESPONSE.getValue()) .message(body.isEmpty() ? response.message() : body) .isJson(false) .build(); } catch (IOException e) { return Status.builder() .statusCode(response.code()) - .code("") + .code(ErrorType.BODY_READ_ERROR.getValue()) .message("[SDK] Failed to read SSE response body: " + e.getMessage()) .isJson(false) .build(); @@ -208,7 +208,7 @@ private Status parseFailed(Response response, Throwable th) { return Status.builder() .statusCode(response.code()) - .code(extractedCode.isEmpty() ? "" : extractedCode) + .code(extractedCode.isEmpty() ? ErrorType.NON_JSON_RESPONSE.getValue() : extractedCode) .message(extractedMessage) .isJson(!extractedCode.isEmpty()) .build(); 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 f257fe8d..4a5dd9da 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java @@ -3,7 +3,7 @@ package com.alibaba.dashscope.protocol.okhttp; 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; @@ -177,9 +177,12 @@ private void establishWebSocketClient( } throw new ApiException( Status.builder() - .code(ErrorType.CONNECTION_ERROR.getValue()) - .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()); } @@ -801,7 +804,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 8562c539..cf71317b 100644 --- a/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java +++ b/src/main/java/com/alibaba/dashscope/threads/runs/DefaultAssistantEventHandler.java @@ -6,10 +6,8 @@ import io.reactivex.Flowable; import io.reactivex.FlowableEmitter; import lombok.Data; -import lombok.extern.slf4j.Slf4j; /** An sample assistant event handler. */ -@Slf4j @Data public class DefaultAssistantEventHandler implements AssistantEventHandler { private AssistantThread assistantThread; @@ -190,7 +188,7 @@ public void onError(String errorMsg) { @Override public void onUnknown(String msg) { - log.warn("Received unknown message: {}", 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 b806d141..9237e457 100644 --- a/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java +++ b/src/main/java/com/alibaba/dashscope/utils/OSSUtils.java @@ -1,6 +1,7 @@ package com.alibaba.dashscope.utils; import com.alibaba.dashscope.common.DashScopeResult; +import com.alibaba.dashscope.common.PublicErrorDef; import com.alibaba.dashscope.common.Status; import com.alibaba.dashscope.exception.ApiException; import com.alibaba.dashscope.exception.NoApiKeyException; @@ -32,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. * @@ -188,10 +199,11 @@ 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("") - .message(response.message()) + .code(matchedDef != null ? matchedDef.getErrorCode() : "") + .message(matchedDef != null ? matchedDef.getErrorMsg() : response.message()) .isJson(isJson) .build(); } @@ -202,10 +214,14 @@ private static Status parseFailed(Response response) { } 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("") - .message(body.isEmpty() ? response.message() : body) + .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/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("今天天气怎么样?"); } From 88f3ed65189fe9c43317b8ea496eb758544beda7 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 21 Jul 2026 15:43:38 +0800 Subject: [PATCH 6/8] fix: resolve NPE in WebSocket message handling causing test hang --- .../okhttp/OkHttpWebSocketClient.java | 92 +++++++++++++------ .../dashscope/TestHalfDuplexWebSocketApi.java | 36 ++++++-- 2 files changed, 94 insertions(+), 34 deletions(-) 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 4a5dd9da..52837f74 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java @@ -283,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: @@ -320,23 +334,37 @@ public void onMessage(WebSocket webSocket, String text) { 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: // Protocol layer error: received undefined event type. @@ -562,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 -> { @@ -569,9 +599,9 @@ public void send(HalfDuplexRequest req, ResultCallback callback this.isFlattenResult = req.getIsFlatten(); }, BackpressureStrategy.BUFFER); - flowable.subscribe().dispose(); - sendBatchRequest(req); - flowable.subscribe( + + // Subscribe first to initialize responseEmitter + Disposable subscription = flowable.subscribe( msg -> { callback.onEvent(msg); }, @@ -584,6 +614,12 @@ public void run() throws Exception { callback.onComplete(); } }); + + // Now send the request - responseEmitter is already initialized and active + sendBatchRequest(req); + + // 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() diff --git a/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java b/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java index 97da5f6c..e5eab381 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); From dbee28b0ebdbb503a872af04509f1e8e130f6f90 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 21 Jul 2026 15:46:21 +0800 Subject: [PATCH 7/8] fix: resolve NPE in WebSocket message handling causing test hang --- .../okhttp/OkHttpWebSocketClient.java | 33 ++++++++++--------- .../dashscope/TestHalfDuplexWebSocketApi.java | 12 +++---- 2 files changed, 23 insertions(+), 22 deletions(-) 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 52837f74..8f409f72 100644 --- a/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java +++ b/src/main/java/com/alibaba/dashscope/protocol/okhttp/OkHttpWebSocketClient.java @@ -599,25 +599,26 @@ public void send(HalfDuplexRequest req, ResultCallback callback this.isFlattenResult = req.getIsFlatten(); }, BackpressureStrategy.BUFFER); - + // 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(); - } - }); - + 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); - + // Note: Don't dispose here - let the WebSocket lifecycle manage the subscription // The subscription will be completed when onClosing/onFailure is called } else { diff --git a/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java b/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java index e5eab381..a6e46944 100644 --- a/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java +++ b/src/test/java/com/alibaba/dashscope/TestHalfDuplexWebSocketApi.java @@ -154,7 +154,7 @@ public void onError(Exception e) { // 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()); + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); } serverListener.assertHalfDuplexRequest(param, StreamingMode.NONE.getValue()); assertEquals(results.get(0).getOutput(), textOutput); @@ -207,7 +207,7 @@ public void onError(Exception e) { // 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()); + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); } serverListener.assertResources(resources); } @@ -253,7 +253,7 @@ public void onError(Exception e) {} // 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()); + throw new AssertionError("Timeout waiting for callback - results size: " + results.size()); } serverListener.assertHalfDuplexRequest(param, StreamingMode.NONE.getValue()); assertEquals(results.get(0).getOutput(), textOutput); @@ -301,7 +301,7 @@ public void onError(Exception e) {} // 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()); + 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)); @@ -349,7 +349,7 @@ public void onError(Exception e) {} // 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()); + 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)); @@ -400,7 +400,7 @@ public void onError(Exception e) {} // 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()); + 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)); From 36905cbfa17d7db999b0ead74d67fbe464e18610 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 23 Jul 2026 14:41:31 +0800 Subject: [PATCH 8/8] refactor: standardize error handling with PublicErrorDef --- .../audio/http_tts/HttpSpeechSynthesizer.java | 6 +-- .../dashscope/common/DashScopeResult.java | 16 +++---- .../alibaba/dashscope/common/ErrorType.java | 26 +++++------ .../dashscope/exception/ApiException.java | 13 ++++-- .../protocol/okhttp/OkHttpHttpClient.java | 46 +++++++++++++------ .../TestHttpProxySettingWithCode.java | 4 +- 6 files changed, 66 insertions(+), 45 deletions(-) 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 1106d602..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(ErrorType.REQUEST_TIMEOUT.getValue()) - .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/common/DashScopeResult.java b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java index 786f8587..b5b51b89 100644 --- a/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java +++ b/src/main/java/com/alibaba/dashscope/common/DashScopeResult.java @@ -9,6 +9,7 @@ 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; @@ -83,15 +84,14 @@ protected T fromResponse(Protocol protocol, NetworkResponse r } else if (response.getHttpStatusCode() >= 400) { throw new ApiException( Status.builder() - .statusCode(response.getHttpStatusCode()) - .code(ErrorType.NON_JSON_RESPONSE.getValue()) + .statusCode(PublicErrorDef.INTERNAL_ERROR.getStatusCode()) + .code(PublicErrorDef.INTERNAL_ERROR.getErrorCode()) .message( - "HTTP " - + response.getHttpStatusCode() - + ": " - + (response.getMessage() != null - ? response.getMessage() - : "No 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()); } diff --git a/src/main/java/com/alibaba/dashscope/common/ErrorType.java b/src/main/java/com/alibaba/dashscope/common/ErrorType.java index 090df407..85dc4ec8 100644 --- a/src/main/java/com/alibaba/dashscope/common/ErrorType.java +++ b/src/main/java/com/alibaba/dashscope/common/ErrorType.java @@ -3,26 +3,22 @@ public enum ErrorType { - /** Network error: DNS failure, connection refused, etc. API did not respond. */ - NETWORK_ERROR("network error"), - - /** WebSocket connection failed after retries. API returned no content. */ - CONNECTION_ERROR("ConnectionError"), + /** The error happens in the response body. */ + RESPONSE_ERROR("response_error"), - /** Asynchronous task polling timed out. API is still processing. */ - TASK_WAIT_TIMEOUT("TaskWaitTimeout"), + /** The error happens because the request is canceled. */ + REQUEST_CANCELLED("request_cancelled"), - /** HTTP TTS waiting for audio data timed out. API returned no error. */ - REQUEST_TIMEOUT("RequestTimeOut"), + /** The error happens because the network protocol is not supported. */ + PROTOCOL_UNSUPPORTED("protocol_unsupported"), - /** JSON parsing failed. Original body preserved in message field. */ - JSON_PARSE_ERROR("json_parse_error"), + /** The api key is not correct. */ + API_KEY_ERROR("api_key_error"), - /** Failed to read response body due to IOException. HTTP status code preserved. */ - BODY_READ_ERROR("body_read_error"), + /** An unknown error. */ + UNKNOWN_ERROR("unknown_error"), - /** Non-JSON content type received (e.g., HTML error page). Original body in message. */ - NON_JSON_RESPONSE("non_json_response"), + NETWORK_ERROR("network error"), ; private final String value; 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 fbd857ad..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; @@ -137,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(); } @@ -151,9 +155,12 @@ private Status parseFailed(Response response, Throwable th) { body = response.body().string(); } catch (IOException e) { return Status.builder() - .statusCode(response.code()) - .code(ErrorType.BODY_READ_ERROR.getValue()) - .message("[SDK] Failed to read response body: " + e.getMessage()) + .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(); } @@ -169,16 +176,24 @@ private Status parseFailed(Response response, Throwable th) { } } return Status.builder() - .statusCode(response.code()) - .code(ErrorType.NON_JSON_RESPONSE.getValue()) - .message(body.isEmpty() ? response.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.BODY_READ_ERROR.getValue()) - .message("[SDK] Failed to read SSE response body: " + e.getMessage()) + .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(); } @@ -208,7 +223,10 @@ private Status parseFailed(Response response, Throwable th) { return Status.builder() .statusCode(response.code()) - .code(extractedCode.isEmpty() ? ErrorType.NON_JSON_RESPONSE.getValue() : extractedCode) + .code( + extractedCode.isEmpty() + ? PublicErrorDef.INTERNAL_ERROR.getErrorCode() + : extractedCode) .message(extractedMessage) .isJson(!extractedCode.isEmpty()) .build(); 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