1010import com .alibaba .dashscope .exception .NoApiKeyException ;
1111import com .alibaba .dashscope .protocol .*;
1212import com .alibaba .dashscope .protocol .Protocol ;
13+ import com .alibaba .dashscope .utils .ApiKeywords ;
1314import com .alibaba .dashscope .utils .Constants ;
1415import com .alibaba .dashscope .utils .JsonUtils ;
1516import com .alibaba .dashscope .utils .StringUtils ;
@@ -184,6 +185,23 @@ private void establishWebSocketClient(
184185 flowable .timeout (60 , TimeUnit .SECONDS ).blockingSubscribe ();
185186 return ;
186187 } catch (Throwable ex ) {
188+ // Unwrap RxJava-wrapped exceptions to find the original ApiException.
189+ Throwable unwrapped = ex ;
190+ while (unwrapped != null && !(unwrapped instanceof ApiException )) {
191+ unwrapped = unwrapped .getCause ();
192+ }
193+
194+ // Client-side errors (e.g. invalid URL, invalid API key) should not be retried or wrapped.
195+ // Rethrow immediately so the caller sees the original error code.
196+ if (unwrapped instanceof ApiException ) {
197+ ApiException apiEx = (ApiException ) unwrapped ;
198+ // Only rethrow 4xx errors directly; 5xx errors should still be retried.
199+ if (apiEx .getStatus () != null
200+ && apiEx .getStatus ().getStatusCode () >= 400
201+ && apiEx .getStatus ().getStatusCode () < 500 ) {
202+ throw apiEx ;
203+ }
204+ }
187205 reconnectionTimes += 1 ;
188206 errorMessage = ex .getMessage ();
189207 log .error (errorMessage );
@@ -256,6 +274,51 @@ public void onClosing(WebSocket webSocket, int code, String reason) {
256274 }
257275 }
258276
277+ /**
278+ * Parse WebSocket handshake failure response body to extract error details. Returns an
279+ * ApiException with the original error code/message if parsing succeeds, otherwise returns null.
280+ */
281+ private ApiException parseWebSocketHandshakeError (
282+ int httpStatusCode , String responseBody , Throwable cause ) {
283+ if (responseBody == null || responseBody .isEmpty ()) {
284+ return null ;
285+ }
286+
287+ try {
288+ JsonObject jsonResponse = JsonUtils .parse (responseBody );
289+ String code = "" ;
290+ String message = "" ;
291+ String requestId = "" ;
292+
293+ if (jsonResponse .has (ApiKeywords .REQUEST_ID )) {
294+ requestId = jsonResponse .get (ApiKeywords .REQUEST_ID ).getAsString ();
295+ }
296+ if (jsonResponse .has (ApiKeywords .CODE ) && !jsonResponse .get (ApiKeywords .CODE ).isJsonNull ()) {
297+ code = jsonResponse .get (ApiKeywords .CODE ).getAsString ();
298+ }
299+ if (jsonResponse .has (ApiKeywords .MESSAGE )) {
300+ message = jsonResponse .get (ApiKeywords .MESSAGE ).getAsString ();
301+ }
302+
303+ // If we have a business error code, use it directly with the HTTP status code
304+ if (!code .isEmpty ()) {
305+ Status status =
306+ Status .builder ()
307+ .statusCode (httpStatusCode )
308+ .code (code )
309+ .message (message )
310+ .requestId (requestId )
311+ .isJson (true )
312+ .build ();
313+ return new ApiException (status , cause );
314+ }
315+ } catch (Throwable e ) {
316+ log .debug ("Failed to parse WebSocket handshake error response as JSON" , e );
317+ }
318+
319+ return null ;
320+ }
321+
259322 @ Override
260323 public void onFailure (WebSocket webSocket , Throwable t , Response response ) {
261324 // Invoked when a web socket has been closed due to an error reading from or
@@ -270,8 +333,10 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) {
270333 }
271334
272335 String responseBody = "" ;
336+ int httpStatusCode = 0 ;
273337 // Get response body if there is.
274338 if (response != null ) {
339+ httpStatusCode = response .code ();
275340 try {
276341 responseBody = response .body ().string ();
277342 } catch (IOException ex ) {
@@ -284,11 +349,23 @@ public void onFailure(WebSocket webSocket, Throwable t, Response response) {
284349 t .getMessage (), t .getCause (), responseBody );
285350 log .error (failureMessage );
286351 isOpen .set (false );
352+
353+ // Try to parse the response body for structured error information
354+ ApiException parsedException = parseWebSocketHandshakeError (httpStatusCode , responseBody , t );
355+
287356 if (connectionEmitter != null && !connectionEmitter .isCancelled ()) {
288- connectionEmitter .onError (new Exception (failureMessage , t ));
357+ if (parsedException != null ) {
358+ connectionEmitter .onError (parsedException );
359+ } else {
360+ connectionEmitter .onError (new Exception (failureMessage , t ));
361+ }
289362 } else if (responseEmitter != null && !responseEmitter .isCancelled ()) {
290363 // error on request
291- responseEmitter .onError (new Exception (failureMessage , t ));
364+ if (parsedException != null ) {
365+ responseEmitter .onError (parsedException );
366+ } else {
367+ responseEmitter .onError (new Exception (failureMessage , t ));
368+ }
292369 } else {
293370 log .error (failureMessage );
294371 }
0 commit comments