Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public RestBasedPage(
this.data = Collections.unmodifiableList(dataExtractionFunction.apply(jsonObject));
this.nextPath = getNextPath(jsonObject);
} catch (Exception e) {
throw new IllegalStateException("Can not parse JSON: " + e);
throw new IllegalStateException("Can not parse JSON", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,28 +56,30 @@ private void handleError(
@NonNull final HttpRequest request, @NonNull final ClientHttpResponse response)
throws IOException {
Objects.requireNonNull(response, "response must not be null");
final String body;
try {
body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
} catch (final Exception e) {
throw new IOException(
"Error (" + response.getStatusCode() + "): " + response.getStatusText(), e);
}

final String error;
try {
final String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
try {
final JsonNode rootNode = objectMapper.readTree(body);
final JsonNode errorNode = rootNode.get("error");
if (errorNode != null) {
error = errorNode.asText();
final JsonNode rootNode = objectMapper.readTree(body);
final JsonNode errorNode = rootNode.get("error");
if (errorNode != null) {
error = errorNode.asText();
} else {
final JsonNode messageNode = rootNode.get("message");
if (messageNode != null) {
error = messageNode.asText();
} else {
final JsonNode messageNode = rootNode.get("message");
if (messageNode != null) {
error = messageNode.asText();
} else {
error = body;
}
error = body;
}
} catch (final Exception e) {
throw new IOException("Error parsing body as JSON: " + body, e);
}
} catch (final Exception e) {
throw new IOException(
"Error (" + response.getStatusCode() + "): " + response.getStatusText());
throw new IOException("Error parsing body as JSON: " + body, e);
}
throw new IOException("Error (" + response.getStatusCode() + "): " + error);
}
Expand Down Expand Up @@ -175,12 +177,7 @@ public ContractVerificationState checkVerification(@NonNull final ContractId con
.onStatus(
HttpStatusCode::is5xxServerError,
(request, response) -> {
throw new RuntimeException(
"Server error ("
+ response.getStatusCode()
+ ") for request '"
+ request.getURI()
+ "'");
handleError(request, response);
})
.body(String.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.function.Function;
import org.hiero.base.HieroException;
import org.hiero.base.implementation.MirrorNodeRestClient;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientException;
import org.springframework.web.util.UriBuilder;

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

public JsonNode doGetCall(Function<UriBuilder, URI> uriFunction) throws HieroException {
final ResponseEntity<String> responseEntity =
restClient
.get()
.uri(uriBuilder -> uriFunction.apply(uriBuilder))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
(request, response) -> {
if (!HttpStatus.NOT_FOUND.equals(response.getStatusCode())
&& !HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
throw new RuntimeException("Client error: " + response.getStatusText());
}
})
.onStatus(
HttpStatusCode::is5xxServerError,
(request, response) -> {
throw new RuntimeException("Server error: " + response.getStatusText());
})
.toEntity(String.class);
final ResponseEntity<String> responseEntity;
try {
responseEntity =
restClient
.get()
.uri(uriBuilder -> uriFunction.apply(uriBuilder))
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(
HttpStatusCode::is4xxClientError,
(request, response) -> {
if (!HttpStatus.NOT_FOUND.equals(response.getStatusCode())
&& !HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
handleError(request, response);
}
})
.onStatus(HttpStatusCode::is5xxServerError, this::handleError)
.toEntity(String.class);
} catch (RestClientException e) {
throw new HieroException("Mirror Node call failed", e);
}
final String body = responseEntity.getBody();
try {
if (HttpStatus.NOT_FOUND.equals(responseEntity.getStatusCode())
Expand All @@ -66,4 +72,16 @@ public JsonNode doGetCall(Function<UriBuilder, URI> uriFunction) throws HieroExc
throw new HieroException("Error parsing body as JSON: " + body, e);
}
}

private void handleError(final HttpRequest request, final ClientHttpResponse response)
throws IOException {
final String body = new String(response.getBody().readAllBytes(), StandardCharsets.UTF_8);
throw new IOException(
"Mirror Node call failed with status "
+ response.getStatusCode()
+ " for request '"
+ request.getURI()
+ "': "
+ body);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.hiero.spring.test;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.hiero.base.config.HieroConfig;
import org.hiero.spring.implementation.ContractVerificationClientImplementation;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.client.ClientHttpResponse;

class ContractVerificationErrorHandlingTest {

@Test
void preservesBodyReadFailureCause() throws Exception {
final ContractVerificationClientImplementation client =
new ContractVerificationClientImplementation(Mockito.mock(HieroConfig.class));
final Method handleError =
ContractVerificationClientImplementation.class.getDeclaredMethod(
"handleError", HttpRequest.class, ClientHttpResponse.class);
handleError.setAccessible(true);
final IOException readFailure = new IOException("stream closed");

final InvocationTargetException exception =
Assertions.assertThrows(
InvocationTargetException.class,
() ->
handleError.invoke(
client, Mockito.mock(HttpRequest.class), failingResponse(readFailure)));

final Throwable cause = exception.getCause();
Assertions.assertInstanceOf(IOException.class, cause);
Assertions.assertSame(readFailure, cause.getCause());
}

private ClientHttpResponse failingResponse(final IOException readFailure) {
return new ClientHttpResponse() {
@Override
public HttpStatusCode getStatusCode() {
return HttpStatus.BAD_GATEWAY;
}

@Override
public String getStatusText() {
return "Bad Gateway";
}

@Override
public void close() {}

@Override
public InputStream getBody() throws IOException {
throw readFailure;
}

@Override
public HttpHeaders getHeaders() {
return HttpHeaders.EMPTY;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.hiero.spring.test;

import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;

import org.hiero.base.HieroException;
import org.hiero.spring.implementation.MirrorNodeRestClientImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestClient;

class MirrorNodeRestClientImplTest {

@Test
void wrapsHttpErrorsInHieroException() {
final RestClient.Builder restClientBuilder =
RestClient.builder().baseUrl("https://mirror.example");
final MockRestServiceServer server = MockRestServiceServer.bindTo(restClientBuilder).build();
final MirrorNodeRestClientImpl client = new MirrorNodeRestClientImpl(restClientBuilder);

server
.expect(requestTo("https://mirror.example/api/v1/network/fees"))
.andRespond(
withStatus(HttpStatus.INTERNAL_SERVER_ERROR)
.contentType(MediaType.TEXT_PLAIN)
.body("upstream failed"));

final HieroException exception =
Assertions.assertThrows(
HieroException.class, () -> client.doGetCall("/api/v1/network/fees"));

Assertions.assertEquals("Mirror Node call failed", exception.getMessage());
Assertions.assertNotNull(exception.getCause());
Assertions.assertTrue(hasCauseMessageContaining(exception, "upstream failed"));
server.verify();
}

private boolean hasCauseMessageContaining(final Throwable throwable, final String text) {
Throwable current = throwable;
while (current != null) {
if (current.getMessage() != null && current.getMessage().contains(text)) {
return true;
}
current = current.getCause();
}
return false;
}
}