Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@
<version>${bouncycastle-version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
package com.volcengine.ark.runtime.exception;


import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;

public class ArkAPIError {

ArkErrorDetails error;
Expand All @@ -22,6 +26,42 @@ public void setError(ArkErrorDetails error) {
this.error = error;
}

/**
* Parses both the standard {"error": {...}} envelope and services that
* return the error details directly. Unknown response shapes retain the
* raw body as the exception message instead of producing a null error.
*/
public static ArkAPIError fromResponseBody(ObjectMapper mapper, String responseBody, String fallbackMessage) {
if (responseBody != null && !responseBody.trim().isEmpty()) {
try {
JsonNode root = mapper.readTree(responseBody);
if (root != null && root.isObject()) {
JsonNode detailsNode = root.get("error");
if (detailsNode == null && root.has("message")) {
detailsNode = root;
}
if (detailsNode != null && detailsNode.isObject()) {
ArkErrorDetails details = mapper.treeToValue(detailsNode, ArkErrorDetails.class);
if (details != null) {
return new ArkAPIError(details);
}
}
}
} catch (IOException ignored) {
// Fall through and preserve the raw response body.
}
}

String message = responseBody;
if (message == null || message.trim().isEmpty()) {
message = fallbackMessage;
}
if (message == null || message.trim().isEmpty()) {
message = "HTTP request failed with an empty response body";
}
return new ArkAPIError(new ArkErrorDetails(message, "", "", "HTTPError"));
}

@Override
public String toString() {
return "ArkAPIError{" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,27 @@ public class ArkHttpException extends RuntimeException {
public final String requestId;

public ArkHttpException(ArkAPIError error, Exception parent, int statusCode, String requestId) {
super(error.error.message, parent);
super(errorMessage(error), parent);
ArkAPIError.ArkErrorDetails details = errorDetails(error);
this.statusCode = statusCode;
this.code = error.error.code;
this.param = error.error.param;
this.type = error.error.type;
this.code = details.getCode();
this.param = details.getParam();
this.type = details.getType();
this.requestId = requestId;
}

private static String errorMessage(ArkAPIError error) {
return errorDetails(error).getMessage();
}

private static ArkAPIError.ArkErrorDetails errorDetails(ArkAPIError error) {
if (error != null && error.getError() != null) {
return error.getError();
}
return new ArkAPIError.ArkErrorDetails(
"HTTP request failed without error details", "", "", "HTTPError");
}

public String getMessage() {
return this.toString();
}
Expand Down
70 changes: 36 additions & 34 deletions src/main/java/com/volcengine/ark/runtime/service/ArkService.java
Original file line number Diff line number Diff line change
Expand Up @@ -220,55 +220,57 @@ public static <T> T execute(Single<T> apiCall) {
T resp = apiCall.blockingGet();
return resp;
} catch (HttpException e) {
String requestId = "";
try {
Headers headers = e.response().raw().request().headers();
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
} catch (Exception ignored) {
}

try {
if (e.response() == null || e.response().errorBody() == null) {
throw e;
}
String errorBody = e.response().errorBody().string();

ArkAPIError error = mapper.readValue(errorBody, ArkAPIError.class);
throw new ArkHttpException(error, e, e.code(), requestId);
} catch (IOException ex) {
throw e;
}
throw translateHttpException(e);
}
}

public static void execute(Completable apiCall) {
try {
apiCall.blockingAwait();
} catch (RuntimeException e) {
if (e instanceof HttpException) {
throw translateHttpException((HttpException) e);
}
Throwable cause = e.getCause();
if (cause instanceof HttpException) {
HttpException he = (HttpException) cause;
String requestId = "";
try {
Headers headers = he.response().raw().request().headers();
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
} catch (Exception ignored) {
}
try {
if (he.response() == null || he.response().errorBody() == null) {
throw he;
}
String errorBody = he.response().errorBody().string();
ArkAPIError error = mapper.readValue(errorBody, ArkAPIError.class);
throw new ArkHttpException(error, he, he.code(), requestId);
} catch (IOException ioe) {
throw new RuntimeException(he);
}
throw translateHttpException(he);
}
throw e;
}
}

private static ArkHttpException translateHttpException(HttpException exception) {
String requestId = requestId(exception);
String responseBody = null;
try {
if (exception.response() != null && exception.response().errorBody() != null) {
responseBody = exception.response().errorBody().string();
}
} catch (IOException ignored) {
// The fallback below retains status and request ID even if reading fails.
}

ArkAPIError error = ArkAPIError.fromResponseBody(mapper, responseBody, exception.getMessage());
return new ArkHttpException(error, exception, exception.code(), requestId);
}

private static String requestId(HttpException exception) {
try {
if (exception.response() != null) {
String serverRequestId = exception.response().headers().get(Const.SERVER_REQUEST_HEADER);
if (serverRequestId != null && !serverRequestId.isEmpty()) {
return serverRequestId;
}
String clientRequestId = exception.response().raw().request().header(Const.CLIENT_REQUEST_HEADER);
return clientRequestId == null ? "" : clientRequestId;
}
} catch (Exception ignored) {
// Return an empty ID when the response does not expose its request.
}
return "";
}

public static Flowable<SSE> stream(Call<ResponseBody> apiCall) {
return stream(apiCall, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)

String requestId = "";
try {
Headers headers = response.raw().request().headers();
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
requestId = response.headers().get(Const.SERVER_REQUEST_HEADER);
if (requestId == null || requestId.isEmpty()) {
Headers headers = response.raw().request().headers();
requestId = headers.get(Const.CLIENT_REQUEST_HEADER);
}
} catch (Exception ignored) {

}
Expand Down Expand Up @@ -80,22 +83,17 @@ public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response)
if (!response.isSuccessful()) {
HttpException e = new HttpException(response);
ResponseBody errorBody = response.errorBody();

if (errorBody == null) {
throw e;
} else {
try {
ArkAPIError error = mapper.readValue(
errorBody.string(),
ArkAPIError.class
);
throw new ArkHttpException(error, e, e.code(), requestId);
} catch (ArkHttpException httpException) {
throw httpException;
} catch (Exception ignore) {
throw new ArkHttpException(new ArkAPIError(new ArkAPIError.ArkErrorDetails(e.getMessage(), "", "", "InternalServiceError")), e, e.code(), requestId);
String responseBody = null;
try {
if (errorBody != null) {
responseBody = errorBody.string();
}
} catch (IOException ignored) {
// Preserve status and request ID even if the body cannot be read.
}
ArkAPIError error = ArkAPIError.fromResponseBody(
mapper, responseBody, e.getMessage());
throw new ArkHttpException(error, e, e.code(), requestId);
}

InputStream in = response.body().byteStream();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
// SPDX-License-Identifier: Apache-2.0

package com.volcengine.ark.runtime.exception;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;

public class ArkAPIErrorTest {
private final ObjectMapper mapper = new ObjectMapper();

@Test
public void parsesWrappedError() {
ArkAPIError error = ArkAPIError.fromResponseBody(
mapper,
"{\"error\":{\"message\":\"model not found\",\"code\":\"InvalidModel\"}}",
"fallback");

assertEquals("model not found", error.getError().getMessage());
assertEquals("InvalidModel", error.getError().getCode());
}

@Test
public void parsesDirectError() {
ArkAPIError error = ArkAPIError.fromResponseBody(
mapper,
"{\"message\":\"model not found\",\"code\":\"InvalidModel\"}",
"fallback");

assertEquals("model not found", error.getError().getMessage());
assertEquals("InvalidModel", error.getError().getCode());
}

@Test
public void preservesNonstandardJsonBody() {
String body = "{\"detail\":\"model is invalid\"}";
ArkAPIError error = ArkAPIError.fromResponseBody(mapper, body, "fallback");
ArkHttpException exception = new ArkHttpException(error, null, 400, "request-id");

assertEquals(body, error.getError().getMessage());
assertEquals(400, exception.statusCode);
assertEquals("request-id", exception.requestId);
assertEquals("HTTPError", exception.code);
}

@Test
public void preservesPlainTextAndEmptyBodies() {
ArkAPIError plain = ArkAPIError.fromResponseBody(mapper, "bad gateway", "fallback");
ArkAPIError empty = ArkAPIError.fromResponseBody(mapper, "", "HTTP 400");

assertEquals("bad gateway", plain.getError().getMessage());
assertEquals("HTTP 400", empty.getError().getMessage());
}

@Test
public void exceptionConstructorHandlesMissingDetails() {
ArkHttpException exception = new ArkHttpException(new ArkAPIError(), null, 400, "request-id");

assertNotNull(exception.getMessage());
assertEquals("HTTPError", exception.code);
assertEquals("request-id", exception.requestId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) 2026 ByteDance Ltd. and/or its affiliates.
// SPDX-License-Identifier: Apache-2.0

package com.volcengine.ark.runtime.service;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.volcengine.ark.runtime.Const;
import com.volcengine.ark.runtime.exception.ArkHttpException;
import io.reactivex.Completable;
import io.reactivex.Single;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.ResponseBody;
import org.junit.Test;
import retrofit2.HttpException;
import retrofit2.Response;

public class ArkServiceErrorTest {
@Test
public void singlePreservesNonstandardBodyStatusAndServerRequestId() {
HttpException source = httpException("{\"detail\":\"model is invalid\"}", "server-request-id");

try {
ArkService.execute(Single.error(source));
fail("expected ArkHttpException");
} catch (ArkHttpException error) {
assertEquals(400, error.statusCode);
assertEquals("server-request-id", error.requestId);
assertTrue(error.getMessage().contains("model is invalid"));
}
}

@Test
public void completablePreservesNonstandardBodyStatusAndServerRequestId() {
HttpException source = httpException("bad request", "server-request-id");

try {
ArkService.execute(Completable.error(source));
fail("expected ArkHttpException");
} catch (ArkHttpException error) {
assertEquals(400, error.statusCode);
assertEquals("server-request-id", error.requestId);
assertTrue(error.getMessage().contains("bad request"));
}
}

private static HttpException httpException(String body, String requestId) {
Request request = new Request.Builder()
.url("https://example.com/api/v3/tokenization")
.header(Const.CLIENT_REQUEST_HEADER, "client-request-id")
.build();
okhttp3.Response rawResponse = new okhttp3.Response.Builder()
.request(request)
.protocol(Protocol.HTTP_1_1)
.code(400)
.message("Bad Request")
.header(Const.SERVER_REQUEST_HEADER, requestId)
.build();
ResponseBody responseBody = ResponseBody.create(
MediaType.get("application/json"), body);
Response<Object> response = Response.error(responseBody, rawResponse);
return new HttpException(response);
}
}
Loading