Skip to content

Commit 4d0ebaf

Browse files
committed
fix: preserve REST error causes
Signed-off-by: Alejandro <26930485+alejandroGM0@users.noreply.github.com>
1 parent 8ce78c9 commit 4d0ebaf

5 files changed

Lines changed: 177 additions & 43 deletions

File tree

hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/RestBasedPage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public RestBasedPage(
6161
this.data = Collections.unmodifiableList(dataExtractionFunction.apply(jsonObject));
6262
this.nextPath = getNextPath(jsonObject);
6363
} catch (Exception e) {
64-
throw new IllegalStateException("Can not parse JSON: " + e);
64+
throw new IllegalStateException("Can not parse JSON", e);
6565
}
6666
}
6767

hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/ContractVerificationClientImplementation.java

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,28 +56,30 @@ private void handleError(
5656
@NonNull final HttpRequest request, @NonNull final ClientHttpResponse response)
5757
throws IOException {
5858
Objects.requireNonNull(response, "response must not be null");
59+
final String body;
60+
try {
61+
body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
62+
} catch (final Exception e) {
63+
throw new IOException(
64+
"Error (" + response.getStatusCode() + "): " + response.getStatusText(), e);
65+
}
66+
5967
final String error;
6068
try {
61-
final String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
62-
try {
63-
final JsonNode rootNode = objectMapper.readTree(body);
64-
final JsonNode errorNode = rootNode.get("error");
65-
if (errorNode != null) {
66-
error = errorNode.asText();
69+
final JsonNode rootNode = objectMapper.readTree(body);
70+
final JsonNode errorNode = rootNode.get("error");
71+
if (errorNode != null) {
72+
error = errorNode.asText();
73+
} else {
74+
final JsonNode messageNode = rootNode.get("message");
75+
if (messageNode != null) {
76+
error = messageNode.asText();
6777
} else {
68-
final JsonNode messageNode = rootNode.get("message");
69-
if (messageNode != null) {
70-
error = messageNode.asText();
71-
} else {
72-
error = body;
73-
}
78+
error = body;
7479
}
75-
} catch (final Exception e) {
76-
throw new IOException("Error parsing body as JSON: " + body, e);
7780
}
7881
} catch (final Exception e) {
79-
throw new IOException(
80-
"Error (" + response.getStatusCode() + "): " + response.getStatusText());
82+
throw new IOException("Error parsing body as JSON: " + body, e);
8183
}
8284
throw new IOException("Error (" + response.getStatusCode() + "): " + error);
8385
}
@@ -175,12 +177,7 @@ public ContractVerificationState checkVerification(@NonNull final ContractId con
175177
.onStatus(
176178
HttpStatusCode::is5xxServerError,
177179
(request, response) -> {
178-
throw new RuntimeException(
179-
"Server error ("
180-
+ response.getStatusCode()
181-
+ ") for request '"
182-
+ request.getURI()
183-
+ "'");
180+
handleError(request, response);
184181
})
185182
.body(String.class);
186183

hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/MirrorNodeRestClientImpl.java

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.databind.JsonNode;
55
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import java.io.IOException;
67
import java.net.URI;
8+
import java.nio.charset.StandardCharsets;
79
import java.util.Objects;
810
import java.util.function.Function;
911
import org.hiero.base.HieroException;
1012
import org.hiero.base.implementation.MirrorNodeRestClient;
13+
import org.springframework.http.HttpRequest;
1114
import org.springframework.http.HttpStatus;
1215
import org.springframework.http.HttpStatusCode;
1316
import org.springframework.http.MediaType;
1417
import org.springframework.http.ResponseEntity;
18+
import org.springframework.http.client.ClientHttpResponse;
1519
import org.springframework.web.client.RestClient;
20+
import org.springframework.web.client.RestClientException;
1621
import org.springframework.web.util.UriBuilder;
1722

1823
public class MirrorNodeRestClientImpl implements MirrorNodeRestClient<JsonNode> {
@@ -32,26 +37,27 @@ public JsonNode doGetCall(String path) throws HieroException {
3237
}
3338

3439
public JsonNode doGetCall(Function<UriBuilder, URI> uriFunction) throws HieroException {
35-
final ResponseEntity<String> responseEntity =
36-
restClient
37-
.get()
38-
.uri(uriBuilder -> uriFunction.apply(uriBuilder))
39-
.accept(MediaType.APPLICATION_JSON)
40-
.retrieve()
41-
.onStatus(
42-
HttpStatusCode::is4xxClientError,
43-
(request, response) -> {
44-
if (!HttpStatus.NOT_FOUND.equals(response.getStatusCode())
45-
&& !HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
46-
throw new RuntimeException("Client error: " + response.getStatusText());
47-
}
48-
})
49-
.onStatus(
50-
HttpStatusCode::is5xxServerError,
51-
(request, response) -> {
52-
throw new RuntimeException("Server error: " + response.getStatusText());
53-
})
54-
.toEntity(String.class);
40+
final ResponseEntity<String> responseEntity;
41+
try {
42+
responseEntity =
43+
restClient
44+
.get()
45+
.uri(uriBuilder -> uriFunction.apply(uriBuilder))
46+
.accept(MediaType.APPLICATION_JSON)
47+
.retrieve()
48+
.onStatus(
49+
HttpStatusCode::is4xxClientError,
50+
(request, response) -> {
51+
if (!HttpStatus.NOT_FOUND.equals(response.getStatusCode())
52+
&& !HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
53+
handleError(request, response);
54+
}
55+
})
56+
.onStatus(HttpStatusCode::is5xxServerError, this::handleError)
57+
.toEntity(String.class);
58+
} catch (RestClientException e) {
59+
throw new HieroException("Mirror Node call failed", e);
60+
}
5561
final String body = responseEntity.getBody();
5662
try {
5763
if (HttpStatus.NOT_FOUND.equals(responseEntity.getStatusCode())
@@ -66,4 +72,16 @@ public JsonNode doGetCall(Function<UriBuilder, URI> uriFunction) throws HieroExc
6672
throw new HieroException("Error parsing body as JSON: " + body, e);
6773
}
6874
}
75+
76+
private void handleError(final HttpRequest request, final ClientHttpResponse response)
77+
throws IOException {
78+
final String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
79+
throw new IOException(
80+
"Mirror Node call failed with status "
81+
+ response.getStatusCode()
82+
+ " for request '"
83+
+ request.getURI()
84+
+ "': "
85+
+ body);
86+
}
6987
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.hiero.spring.test;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.lang.reflect.InvocationTargetException;
6+
import java.lang.reflect.Method;
7+
import org.hiero.base.config.HieroConfig;
8+
import org.hiero.spring.implementation.ContractVerificationClientImplementation;
9+
import org.junit.jupiter.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
import org.mockito.Mockito;
12+
import org.springframework.http.HttpHeaders;
13+
import org.springframework.http.HttpRequest;
14+
import org.springframework.http.HttpStatus;
15+
import org.springframework.http.HttpStatusCode;
16+
import org.springframework.http.client.ClientHttpResponse;
17+
18+
class ContractVerificationErrorHandlingTest {
19+
20+
@Test
21+
void preservesBodyReadFailureCause() throws Exception {
22+
final ContractVerificationClientImplementation client =
23+
new ContractVerificationClientImplementation(Mockito.mock(HieroConfig.class));
24+
final Method handleError =
25+
ContractVerificationClientImplementation.class.getDeclaredMethod(
26+
"handleError", HttpRequest.class, ClientHttpResponse.class);
27+
handleError.setAccessible(true);
28+
final IOException readFailure = new IOException("stream closed");
29+
30+
final InvocationTargetException exception =
31+
Assertions.assertThrows(
32+
InvocationTargetException.class,
33+
() ->
34+
handleError.invoke(
35+
client, Mockito.mock(HttpRequest.class), failingResponse(readFailure)));
36+
37+
final Throwable cause = exception.getCause();
38+
Assertions.assertInstanceOf(IOException.class, cause);
39+
Assertions.assertSame(readFailure, cause.getCause());
40+
}
41+
42+
private ClientHttpResponse failingResponse(final IOException readFailure) {
43+
return new ClientHttpResponse() {
44+
@Override
45+
public HttpStatusCode getStatusCode() {
46+
return HttpStatus.BAD_GATEWAY;
47+
}
48+
49+
@Override
50+
public String getStatusText() {
51+
return "Bad Gateway";
52+
}
53+
54+
@Override
55+
public void close() {}
56+
57+
@Override
58+
public InputStream getBody() throws IOException {
59+
throw readFailure;
60+
}
61+
62+
@Override
63+
public HttpHeaders getHeaders() {
64+
return HttpHeaders.EMPTY;
65+
}
66+
};
67+
}
68+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.hiero.spring.test;
2+
3+
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
4+
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
5+
6+
import org.hiero.base.HieroException;
7+
import org.hiero.spring.implementation.MirrorNodeRestClientImpl;
8+
import org.junit.jupiter.api.Assertions;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.test.web.client.MockRestServiceServer;
13+
import org.springframework.web.client.RestClient;
14+
15+
class MirrorNodeRestClientImplTest {
16+
17+
@Test
18+
void wrapsHttpErrorsInHieroException() {
19+
final RestClient.Builder restClientBuilder =
20+
RestClient.builder().baseUrl("https://mirror.example");
21+
final MockRestServiceServer server = MockRestServiceServer.bindTo(restClientBuilder).build();
22+
final MirrorNodeRestClientImpl client = new MirrorNodeRestClientImpl(restClientBuilder);
23+
24+
server
25+
.expect(requestTo("https://mirror.example/api/v1/network/fees"))
26+
.andRespond(
27+
withStatus(HttpStatus.INTERNAL_SERVER_ERROR)
28+
.contentType(MediaType.TEXT_PLAIN)
29+
.body("upstream failed"));
30+
31+
final HieroException exception =
32+
Assertions.assertThrows(
33+
HieroException.class, () -> client.doGetCall("/api/v1/network/fees"));
34+
35+
Assertions.assertEquals("Mirror Node call failed", exception.getMessage());
36+
Assertions.assertNotNull(exception.getCause());
37+
Assertions.assertTrue(hasCauseMessageContaining(exception, "upstream failed"));
38+
server.verify();
39+
}
40+
41+
private boolean hasCauseMessageContaining(final Throwable throwable, final String text) {
42+
Throwable current = throwable;
43+
while (current != null) {
44+
if (current.getMessage() != null && current.getMessage().contains(text)) {
45+
return true;
46+
}
47+
current = current.getCause();
48+
}
49+
return false;
50+
}
51+
}

0 commit comments

Comments
 (0)