Skip to content

Commit e3ff2ad

Browse files
committed
fix: preserve nonstandard API error responses
Sync-Source-Commit: b77319c0d2e7684f58f620668e5bca5c8a73dd40 Ark-APIs-Commit: 665a2441c2c8d9342e4770d776ea19de36ef6f83 Hand-Written-Reason: Manual error-handling fix (preserve nonstandard API error responses). No ark-apis regeneration involved; attributed to the same ark-apis snapshot as parent ef0cb7e. Release-Version: 0.3.0
1 parent e416245 commit e3ff2ad

7 files changed

Lines changed: 247 additions & 54 deletions

File tree

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@
114114
<version>${bouncycastle-version}</version>
115115
<optional>true</optional>
116116
</dependency>
117+
<dependency>
118+
<groupId>junit</groupId>
119+
<artifactId>junit</artifactId>
120+
<version>4.13.2</version>
121+
<scope>test</scope>
122+
</dependency>
117123
</dependencies>
118124

119125
<build>

src/main/java/com/volcengine/ark/runtime/exception/ArkAPIError.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
package com.volcengine.ark.runtime.exception;
55

66

7+
import com.fasterxml.jackson.databind.JsonNode;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import java.io.IOException;
10+
711
public class ArkAPIError {
812

913
ArkErrorDetails error;
@@ -22,6 +26,42 @@ public void setError(ArkErrorDetails error) {
2226
this.error = error;
2327
}
2428

29+
/**
30+
* Parses both the standard {"error": {...}} envelope and services that
31+
* return the error details directly. Unknown response shapes retain the
32+
* raw body as the exception message instead of producing a null error.
33+
*/
34+
public static ArkAPIError fromResponseBody(ObjectMapper mapper, String responseBody, String fallbackMessage) {
35+
if (responseBody != null && !responseBody.trim().isEmpty()) {
36+
try {
37+
JsonNode root = mapper.readTree(responseBody);
38+
if (root != null && root.isObject()) {
39+
JsonNode detailsNode = root.get("error");
40+
if (detailsNode == null && root.has("message")) {
41+
detailsNode = root;
42+
}
43+
if (detailsNode != null && detailsNode.isObject()) {
44+
ArkErrorDetails details = mapper.treeToValue(detailsNode, ArkErrorDetails.class);
45+
if (details != null) {
46+
return new ArkAPIError(details);
47+
}
48+
}
49+
}
50+
} catch (IOException ignored) {
51+
// Fall through and preserve the raw response body.
52+
}
53+
}
54+
55+
String message = responseBody;
56+
if (message == null || message.trim().isEmpty()) {
57+
message = fallbackMessage;
58+
}
59+
if (message == null || message.trim().isEmpty()) {
60+
message = "HTTP request failed with an empty response body";
61+
}
62+
return new ArkAPIError(new ArkErrorDetails(message, "", "", "HTTPError"));
63+
}
64+
2565
@Override
2666
public String toString() {
2767
return "ArkAPIError{" +

src/main/java/com/volcengine/ark/runtime/exception/ArkHttpException.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,27 @@ public class ArkHttpException extends RuntimeException {
1818
public final String requestId;
1919

2020
public ArkHttpException(ArkAPIError error, Exception parent, int statusCode, String requestId) {
21-
super(error.error.message, parent);
21+
super(errorMessage(error), parent);
22+
ArkAPIError.ArkErrorDetails details = errorDetails(error);
2223
this.statusCode = statusCode;
23-
this.code = error.error.code;
24-
this.param = error.error.param;
25-
this.type = error.error.type;
24+
this.code = details.getCode();
25+
this.param = details.getParam();
26+
this.type = details.getType();
2627
this.requestId = requestId;
2728
}
2829

30+
private static String errorMessage(ArkAPIError error) {
31+
return errorDetails(error).getMessage();
32+
}
33+
34+
private static ArkAPIError.ArkErrorDetails errorDetails(ArkAPIError error) {
35+
if (error != null && error.getError() != null) {
36+
return error.getError();
37+
}
38+
return new ArkAPIError.ArkErrorDetails(
39+
"HTTP request failed without error details", "", "", "HTTPError");
40+
}
41+
2942
public String getMessage() {
3043
return this.toString();
3144
}

src/main/java/com/volcengine/ark/runtime/service/ArkService.java

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -220,55 +220,57 @@ public static <T> T execute(Single<T> apiCall) {
220220
T resp = apiCall.blockingGet();
221221
return resp;
222222
} catch (HttpException e) {
223-
String requestId = "";
224-
try {
225-
Headers headers = e.response().raw().request().headers();
226-
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
227-
} catch (Exception ignored) {
228-
}
229-
230-
try {
231-
if (e.response() == null || e.response().errorBody() == null) {
232-
throw e;
233-
}
234-
String errorBody = e.response().errorBody().string();
235-
236-
ArkAPIError error = mapper.readValue(errorBody, ArkAPIError.class);
237-
throw new ArkHttpException(error, e, e.code(), requestId);
238-
} catch (IOException ex) {
239-
throw e;
240-
}
223+
throw translateHttpException(e);
241224
}
242225
}
243226

244227
public static void execute(Completable apiCall) {
245228
try {
246229
apiCall.blockingAwait();
247230
} catch (RuntimeException e) {
231+
if (e instanceof HttpException) {
232+
throw translateHttpException((HttpException) e);
233+
}
248234
Throwable cause = e.getCause();
249235
if (cause instanceof HttpException) {
250236
HttpException he = (HttpException) cause;
251-
String requestId = "";
252-
try {
253-
Headers headers = he.response().raw().request().headers();
254-
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
255-
} catch (Exception ignored) {
256-
}
257-
try {
258-
if (he.response() == null || he.response().errorBody() == null) {
259-
throw he;
260-
}
261-
String errorBody = he.response().errorBody().string();
262-
ArkAPIError error = mapper.readValue(errorBody, ArkAPIError.class);
263-
throw new ArkHttpException(error, he, he.code(), requestId);
264-
} catch (IOException ioe) {
265-
throw new RuntimeException(he);
266-
}
237+
throw translateHttpException(he);
267238
}
268239
throw e;
269240
}
270241
}
271242

243+
private static ArkHttpException translateHttpException(HttpException exception) {
244+
String requestId = requestId(exception);
245+
String responseBody = null;
246+
try {
247+
if (exception.response() != null && exception.response().errorBody() != null) {
248+
responseBody = exception.response().errorBody().string();
249+
}
250+
} catch (IOException ignored) {
251+
// The fallback below retains status and request ID even if reading fails.
252+
}
253+
254+
ArkAPIError error = ArkAPIError.fromResponseBody(mapper, responseBody, exception.getMessage());
255+
return new ArkHttpException(error, exception, exception.code(), requestId);
256+
}
257+
258+
private static String requestId(HttpException exception) {
259+
try {
260+
if (exception.response() != null) {
261+
String serverRequestId = exception.response().headers().get(Const.SERVER_REQUEST_HEADER);
262+
if (serverRequestId != null && !serverRequestId.isEmpty()) {
263+
return serverRequestId;
264+
}
265+
String clientRequestId = exception.response().raw().request().header(Const.CLIENT_REQUEST_HEADER);
266+
return clientRequestId == null ? "" : clientRequestId;
267+
}
268+
} catch (Exception ignored) {
269+
// Return an empty ID when the response does not expose its request.
270+
}
271+
return "";
272+
}
273+
272274
public static Flowable<SSE> stream(Call<ResponseBody> apiCall) {
273275
return stream(apiCall, false);
274276
}

src/main/java/com/volcengine/ark/runtime/utils/ResponseBodyCallback.java

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)
5050

5151
String requestId = "";
5252
try {
53-
Headers headers = response.raw().request().headers();
54-
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
53+
requestId = response.headers().get(Const.SERVER_REQUEST_HEADER);
54+
if (requestId == null || requestId.isEmpty()) {
55+
Headers headers = response.raw().request().headers();
56+
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
57+
}
5558
} catch (Exception ignored) {
5659

5760
}
@@ -80,22 +83,17 @@ public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)
8083
if (!response.isSuccessful()) {
8184
HttpException e = new HttpException(response);
8285
ResponseBody errorBody = response.errorBody();
83-
84-
if (errorBody == null) {
85-
throw e;
86-
} else {
87-
try {
88-
ArkAPIError error = mapper.readValue(
89-
errorBody.string(),
90-
ArkAPIError.class
91-
);
92-
throw new ArkHttpException(error, e, e.code(), requestId);
93-
} catch (ArkHttpException httpException) {
94-
throw httpException;
95-
} catch (Exception ignore) {
96-
throw new ArkHttpException(new ArkAPIError(new ArkAPIError.ArkErrorDetails(e.getMessage(), "", "", "InternalServiceError")), e, e.code(), requestId);
86+
String responseBody = null;
87+
try {
88+
if (errorBody != null) {
89+
responseBody = errorBody.string();
9790
}
91+
} catch (IOException ignored) {
92+
// Preserve status and request ID even if the body cannot be read.
9893
}
94+
ArkAPIError error = ArkAPIError.fromResponseBody(
95+
mapper, responseBody, e.getMessage());
96+
throw new ArkHttpException(error, e, e.code(), requestId);
9997
}
10098

10199
InputStream in = response.body().byteStream();
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.volcengine.ark.runtime.exception;
5+
6+
import static org.junit.Assert.assertEquals;
7+
import static org.junit.Assert.assertNotNull;
8+
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import org.junit.Test;
11+
12+
public class ArkAPIErrorTest {
13+
private final ObjectMapper mapper = new ObjectMapper();
14+
15+
@Test
16+
public void parsesWrappedError() {
17+
ArkAPIError error = ArkAPIError.fromResponseBody(
18+
mapper,
19+
"{\"error\":{\"message\":\"model not found\",\"code\":\"InvalidModel\"}}",
20+
"fallback");
21+
22+
assertEquals("model not found", error.getError().getMessage());
23+
assertEquals("InvalidModel", error.getError().getCode());
24+
}
25+
26+
@Test
27+
public void parsesDirectError() {
28+
ArkAPIError error = ArkAPIError.fromResponseBody(
29+
mapper,
30+
"{\"message\":\"model not found\",\"code\":\"InvalidModel\"}",
31+
"fallback");
32+
33+
assertEquals("model not found", error.getError().getMessage());
34+
assertEquals("InvalidModel", error.getError().getCode());
35+
}
36+
37+
@Test
38+
public void preservesNonstandardJsonBody() {
39+
String body = "{\"detail\":\"model is invalid\"}";
40+
ArkAPIError error = ArkAPIError.fromResponseBody(mapper, body, "fallback");
41+
ArkHttpException exception = new ArkHttpException(error, null, 400, "request-id");
42+
43+
assertEquals(body, error.getError().getMessage());
44+
assertEquals(400, exception.statusCode);
45+
assertEquals("request-id", exception.requestId);
46+
assertEquals("HTTPError", exception.code);
47+
}
48+
49+
@Test
50+
public void preservesPlainTextAndEmptyBodies() {
51+
ArkAPIError plain = ArkAPIError.fromResponseBody(mapper, "bad gateway", "fallback");
52+
ArkAPIError empty = ArkAPIError.fromResponseBody(mapper, "", "HTTP 400");
53+
54+
assertEquals("bad gateway", plain.getError().getMessage());
55+
assertEquals("HTTP 400", empty.getError().getMessage());
56+
}
57+
58+
@Test
59+
public void exceptionConstructorHandlesMissingDetails() {
60+
ArkHttpException exception = new ArkHttpException(new ArkAPIError(), null, 400, "request-id");
61+
62+
assertNotNull(exception.getMessage());
63+
assertEquals("HTTPError", exception.code);
64+
assertEquals("request-id", exception.requestId);
65+
}
66+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package com.volcengine.ark.runtime.service;
5+
6+
import static org.junit.Assert.assertEquals;
7+
import static org.junit.Assert.assertTrue;
8+
import static org.junit.Assert.fail;
9+
10+
import com.volcengine.ark.runtime.Const;
11+
import com.volcengine.ark.runtime.exception.ArkHttpException;
12+
import io.reactivex.Completable;
13+
import io.reactivex.Single;
14+
import okhttp3.MediaType;
15+
import okhttp3.Protocol;
16+
import okhttp3.Request;
17+
import okhttp3.ResponseBody;
18+
import org.junit.Test;
19+
import retrofit2.HttpException;
20+
import retrofit2.Response;
21+
22+
public class ArkServiceErrorTest {
23+
@Test
24+
public void singlePreservesNonstandardBodyStatusAndServerRequestId() {
25+
HttpException source = httpException("{\"detail\":\"model is invalid\"}", "server-request-id");
26+
27+
try {
28+
ArkService.execute(Single.error(source));
29+
fail("expected ArkHttpException");
30+
} catch (ArkHttpException error) {
31+
assertEquals(400, error.statusCode);
32+
assertEquals("server-request-id", error.requestId);
33+
assertTrue(error.getMessage().contains("model is invalid"));
34+
}
35+
}
36+
37+
@Test
38+
public void completablePreservesNonstandardBodyStatusAndServerRequestId() {
39+
HttpException source = httpException("bad request", "server-request-id");
40+
41+
try {
42+
ArkService.execute(Completable.error(source));
43+
fail("expected ArkHttpException");
44+
} catch (ArkHttpException error) {
45+
assertEquals(400, error.statusCode);
46+
assertEquals("server-request-id", error.requestId);
47+
assertTrue(error.getMessage().contains("bad request"));
48+
}
49+
}
50+
51+
private static HttpException httpException(String body, String requestId) {
52+
Request request = new Request.Builder()
53+
.url("https://example.com/api/v3/tokenization")
54+
.header(Const.CLIENT_REQUEST_HEADER, "client-request-id")
55+
.build();
56+
okhttp3.Response rawResponse = new okhttp3.Response.Builder()
57+
.request(request)
58+
.protocol(Protocol.HTTP_1_1)
59+
.code(400)
60+
.message("Bad Request")
61+
.header(Const.SERVER_REQUEST_HEADER, requestId)
62+
.build();
63+
ResponseBody responseBody = ResponseBody.create(
64+
MediaType.get("application/json"), body);
65+
Response<Object> response = Response.error(responseBody, rawResponse);
66+
return new HttpException(response);
67+
}
68+
}

0 commit comments

Comments
 (0)