diff --git a/pom.xml b/pom.xml index 324801b..c9854c4 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ dev.bannmann.restflow restflow - 0.10 + 0.11 ${project.groupId}:${project.artifactId} Fluent API for Java 11 HTTP Client with JSON-B support diff --git a/src/main/java/dev/bannmann/restflow/AbstractRequester.java b/src/main/java/dev/bannmann/restflow/AbstractRequester.java index d97ef7e..826d531 100644 --- a/src/main/java/dev/bannmann/restflow/AbstractRequester.java +++ b/src/main/java/dev/bannmann/restflow/AbstractRequester.java @@ -93,7 +93,23 @@ private HttpResponse failOrPassThrough(HttpResponse response) protected abstract void verifyNoErrors(HttpResponse response); - protected abstract R extractValue(HttpResponse response); + private R extractValue(HttpResponse httpResponse) + { + try + { + return doExtractValue(httpResponse); + } + catch (RuntimeException e) + { + String message = String.format("Could not process response to %s %s:\n%s", + request.method(), + request.uri(), + httpResponse.body()); + throw new ResponseBodyException(message, e, httpResponse, diagnosticsData, callerFrames); + } + } + + protected abstract R doExtractValue(HttpResponse response); protected boolean isFailure(int responseStatus) { @@ -105,7 +121,7 @@ protected boolean isSuccess(int responseStatus) return responseStatus >= 200 && responseStatus < 300; } - protected RequestStatusException createException(HttpResponse response) + protected ResponseStatusException createException(HttpResponse response) { int status = response.statusCode(); String body = getStringBody(response); @@ -115,12 +131,9 @@ protected RequestStatusException createException(HttpResponse response) request.method(), request.uri()); - return RequestStatusException.builder() + return ResponseStatusException.builder() .message(message) - .request(request) .response(response) - .status(status) - .body(body) .diagnosticsData(diagnosticsData) .build(); } diff --git a/src/main/java/dev/bannmann/restflow/InvalidResponseException.java b/src/main/java/dev/bannmann/restflow/InvalidResponseException.java new file mode 100644 index 0000000..3c19ff8 --- /dev/null +++ b/src/main/java/dev/bannmann/restflow/InvalidResponseException.java @@ -0,0 +1,49 @@ +package dev.bannmann.restflow; + +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; + +import lombok.Getter; + +/** + * Thrown when the response is invalid. Refer to subclasses for specific cases. + */ +@Getter +public abstract class InvalidResponseException extends RequestException +{ + private final transient HttpResponse response; + + protected InvalidResponseException( + String message, + Throwable cause, + HttpResponse response, + Map diagnosticsData, + List callerFrames) + { + super(message, cause, diagnosticsData, callerFrames); + this.response = response; + } + + @Override + public HttpRequest getRequest() + { + return response.request(); + } + + public int getStatusCode() + { + return response.statusCode(); + } + + public String getRawBody() + { + Object body = response.body(); + if (body == null) + { + return null; + } + return body.toString(); + } +} diff --git a/src/main/java/dev/bannmann/restflow/OptionalRequester.java b/src/main/java/dev/bannmann/restflow/OptionalRequester.java index a4b3087..3dcf59b 100644 --- a/src/main/java/dev/bannmann/restflow/OptionalRequester.java +++ b/src/main/java/dev/bannmann/restflow/OptionalRequester.java @@ -38,7 +38,7 @@ protected void verifyNoErrors(HttpResponse response) } @Override - protected Optional extractValue(HttpResponse response) + protected Optional doExtractValue(HttpResponse response) { if (response.statusCode() == HttpStatus.NOT_FOUND) { diff --git a/src/main/java/dev/bannmann/restflow/RegularRequester.java b/src/main/java/dev/bannmann/restflow/RegularRequester.java index 587fbf9..9dd6954 100644 --- a/src/main/java/dev/bannmann/restflow/RegularRequester.java +++ b/src/main/java/dev/bannmann/restflow/RegularRequester.java @@ -35,7 +35,7 @@ protected void verifyNoErrors(HttpResponse response) } @Override - protected R extractValue(HttpResponse response) + protected R doExtractValue(HttpResponse response) { return spec.getResponseBodyConfig() .getResponseConverter() diff --git a/src/main/java/dev/bannmann/restflow/RequestException.java b/src/main/java/dev/bannmann/restflow/RequestException.java index 5c9f020..8482069 100644 --- a/src/main/java/dev/bannmann/restflow/RequestException.java +++ b/src/main/java/dev/bannmann/restflow/RequestException.java @@ -7,26 +7,24 @@ import lombok.Getter; /** - * Thrown when the request could not be completed for some reason. Refer to {@link RequestStatusException} and - * {@link RequestFailureException} for specific cases. + * Thrown when the request could not be completed for some reason. Refer to subclasses for specific cases. */ @Getter public abstract class RequestException extends RuntimeException { - private final transient HttpRequest request; private final transient Map diagnosticsData; private final transient List callerFrames; protected RequestException( - HttpRequest request, String message, Throwable cause, Map diagnosticsData, List callerFrames) { super(message, cause); - this.request = request; this.diagnosticsData = diagnosticsData; this.callerFrames = callerFrames; } + + public abstract HttpRequest getRequest(); } diff --git a/src/main/java/dev/bannmann/restflow/RequestFailureException.java b/src/main/java/dev/bannmann/restflow/RequestFailureException.java index c1863b2..90ba0d3 100644 --- a/src/main/java/dev/bannmann/restflow/RequestFailureException.java +++ b/src/main/java/dev/bannmann/restflow/RequestFailureException.java @@ -4,13 +4,16 @@ import java.util.List; import java.util.Map; +import lombok.Getter; + /** * Thrown when the request failed without a response. - * - * @see RequestStatusException */ +@Getter public class RequestFailureException extends RequestException { + private final HttpRequest request; + public RequestFailureException( HttpRequest request, String message, @@ -18,6 +21,7 @@ public RequestFailureException( Map diagnosticsData, List callerFrames) { - super(request, message, cause, diagnosticsData, callerFrames); + super(message, cause, diagnosticsData, callerFrames); + this.request = request; } } diff --git a/src/main/java/dev/bannmann/restflow/RequestStatusException.java b/src/main/java/dev/bannmann/restflow/RequestStatusException.java deleted file mode 100644 index b2cfad8..0000000 --- a/src/main/java/dev/bannmann/restflow/RequestStatusException.java +++ /dev/null @@ -1,40 +0,0 @@ -package dev.bannmann.restflow; - -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.util.List; -import java.util.Map; - -import lombok.Builder; -import lombok.Getter; - -/** - * Thrown when the response to a request has a non-2xx status code. - * - * @see RequestFailureException - */ -@Getter -public class RequestStatusException extends RequestException -{ - private final int status; - private final transient HttpResponse response; - private final String body; - - @Builder - public RequestStatusException( - String message, - Throwable cause, - HttpRequest request, - HttpResponse response, - int status, - String body, - Map diagnosticsData, - List callerFrames) - { - super(request, message, cause, diagnosticsData, callerFrames); - - this.response = response; - this.status = status; - this.body = body; - } -} diff --git a/src/main/java/dev/bannmann/restflow/ResponseBodyException.java b/src/main/java/dev/bannmann/restflow/ResponseBodyException.java new file mode 100644 index 0000000..332a139 --- /dev/null +++ b/src/main/java/dev/bannmann/restflow/ResponseBodyException.java @@ -0,0 +1,24 @@ +package dev.bannmann.restflow; + +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; + +import lombok.Builder; + +/** + * Thrown when the response body could not be processed. + */ +public class ResponseBodyException extends InvalidResponseException +{ + @Builder + public ResponseBodyException( + String message, + Throwable cause, + HttpResponse response, + Map diagnosticsData, + List callerFrames) + { + super(message, cause, response, diagnosticsData, callerFrames); + } +} diff --git a/src/main/java/dev/bannmann/restflow/ResponseStatusException.java b/src/main/java/dev/bannmann/restflow/ResponseStatusException.java new file mode 100644 index 0000000..3f89679 --- /dev/null +++ b/src/main/java/dev/bannmann/restflow/ResponseStatusException.java @@ -0,0 +1,24 @@ +package dev.bannmann.restflow; + +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; + +import lombok.Builder; + +/** + * Thrown when the response to a request has a non-2xx status code. + */ +public class ResponseStatusException extends InvalidResponseException +{ + @Builder + public ResponseStatusException( + String message, + Throwable cause, + HttpResponse response, + Map diagnosticsData, + List callerFrames) + { + super(message, cause, response, diagnosticsData, callerFrames); + } +} diff --git a/src/test/java/dev/bannmann/restflow/TestBasicRestClient.java b/src/test/java/dev/bannmann/restflow/TestBasicRestClient.java index 6aa1ae1..df07d2b 100644 --- a/src/test/java/dev/bannmann/restflow/TestBasicRestClient.java +++ b/src/test/java/dev/bannmann/restflow/TestBasicRestClient.java @@ -21,7 +21,9 @@ import java.util.function.Function; import javax.json.Json; +import javax.json.JsonObject; import javax.json.bind.JsonbBuilder; +import javax.json.stream.JsonParsingException; import lombok.AllArgsConstructor; import lombok.Data; @@ -185,6 +187,22 @@ public void testTryFetchServerError() assertThrowsInternalServerError(responseFuture, "POST"); } + @Test(timeOut = METHOD_TIMEOUT) + public void testExecuteWithMalformedJsonResponse() + { + mockedServer.when(TestData.Requests.Incoming.POST) + .respond(TestData.Responses.HTML_ERROR_PAGE_WITH_SUCCESS_STATUS); + + CompletableFuture> responseFuture = makeClient().make(TestData.Requests.Outgoing.POST) + .returningJsonObject() + .tryFetch(); + + assertThatThrownBy(responseFuture::get).isExactlyInstanceOf(ExecutionException.class) + .extracting(Throwable::getCause, as(InstanceOfAssertFactories.THROWABLE)) + .isExactlyInstanceOf(ResponseBodyException.class) + .hasRootCauseExactlyInstanceOf(JsonParsingException.class); + } + private void assertThrowsInternalServerError(CompletableFuture responseFuture, String method) { assertThrowsRequestStatusException(responseFuture, @@ -197,13 +215,13 @@ private void assertThrowsInternalServerError(CompletableFuture responseFuture private void assertThrowsRequestStatusException( CompletableFuture responseFuture, int status, String path, String body, String method) { - String message = String.format("Got status %d with message '%s' for %s %s%s", + var message = String.format("Got status %d with message '%s' for %s %s%s", status, body, method, TestData.BASE_URL, path); - RequestStatusException expectedCause = RequestStatusException.builder() + var expectedCause = ResponseStatusException.builder() .message(message) .build(); diff --git a/src/test/java/dev/bannmann/restflow/TestData.java b/src/test/java/dev/bannmann/restflow/TestData.java index f641fa7..8633368 100644 --- a/src/test/java/dev/bannmann/restflow/TestData.java +++ b/src/test/java/dev/bannmann/restflow/TestData.java @@ -72,6 +72,10 @@ public class Body public final String INTERNAL_SERVER_ERROR_BODY = "Detected a slight field variance in the thera-magnetic caesium portal housing."; + + public final String + HTML_UNEXPECTED_ERROR + = "Sorry, an unexpected error occurred. Please try again later."; } public final org.mockserver.model.HttpResponse NO_CONTENT = response().withStatusCode(204); @@ -85,6 +89,10 @@ public class Body public final org.mockserver.model.HttpResponse HELLO_WORLD_ARRAY = response().withStatusCode(200) .withBody(Body.HELLO_WORLD_JSON_ARRAY); + public final org.mockserver.model.HttpResponse HTML_ERROR_PAGE_WITH_SUCCESS_STATUS = response().withStatusCode( + 200) + .withBody(Body.HTML_UNEXPECTED_ERROR); + public final org.mockserver.model.HttpResponse SERVER_BUSY = response().withStatusCode(429) .withBody("Please try again later") .withDelay(TimeUnit.MILLISECONDS, 500);