diff --git a/http-client/src/main/java/io/avaje/http/client/DHttpClientContext.java b/http-client/src/main/java/io/avaje/http/client/DHttpClientContext.java index fff7a101b..2d405f2f2 100644 --- a/http-client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/http-client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -15,6 +15,8 @@ import java.util.concurrent.atomic.LongAccumulator; import java.util.concurrent.atomic.LongAdder; import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; final class DHttpClientContext implements HttpClient, SpiHttpClient { @@ -200,7 +202,7 @@ public long avgMicros() { } @SuppressWarnings("unchecked") - public BodyContent readErrorContent(boolean responseAsBytes, HttpResponse httpResponse) { + BodyContent readErrorContent(boolean responseAsBytes, HttpResponse httpResponse) { if (responseAsBytes) { return readContent((HttpResponse) httpResponse); } @@ -209,6 +211,13 @@ public BodyContent readErrorContent(boolean responseAsBytes, HttpResponse htt if (body instanceof String) { return new BodyContent(contentType, ((String) body).getBytes(StandardCharsets.UTF_8)); } + if (body instanceof Stream) { + var sb = new StringBuilder(50); + for (Object line : ((Stream) body).collect(Collectors.toList())) { + sb.append(line); + } + return new BodyContent(contentType, sb.toString().getBytes(StandardCharsets.UTF_8)); + } final String type = (body == null) ? "null" : body.getClass().toString(); throw new IllegalStateException("Unable to translate response body to bytes? Maybe use HttpResponse directly instead? Response body type: " + type); } diff --git a/http-client/src/main/java/io/avaje/http/client/HttpException.java b/http-client/src/main/java/io/avaje/http/client/HttpException.java index 360247588..a6c8f8ab3 100644 --- a/http-client/src/main/java/io/avaje/http/client/HttpException.java +++ b/http-client/src/main/java/io/avaje/http/client/HttpException.java @@ -2,6 +2,7 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; +import java.util.Optional; /** * HTTP Exception with support for converting the error response body into a bean. @@ -151,4 +152,18 @@ public HttpResponse httpResponse() { return httpResponse; } + /** + * Return the response Content-Type header. + */ + public Optional contentType() { + return httpResponse.headers().firstValue("Content-Type"); + } + + /** + * Return true if the Content-Type is text/plain. + */ + public boolean isPlainText() { + return "text/plain".equals(contentType().orElse(null)); + } + } diff --git a/http-client/src/test/java/io/avaje/http/client/HelloControllerTest.java b/http-client/src/test/java/io/avaje/http/client/HelloControllerTest.java index c90bdc568..1beebb1d8 100644 --- a/http-client/src/test/java/io/avaje/http/client/HelloControllerTest.java +++ b/http-client/src/test/java/io/avaje/http/client/HelloControllerTest.java @@ -286,6 +286,12 @@ void get_stream_NotFoundException() { assertThat(metrics.errorCount()).isEqualTo(1); assertThat(metrics.responseBytes()).isEqualTo(0); assertThat(metrics.totalMicros()).isGreaterThan(0); + + assertThat(httpException.bodyAsString()).isEqualTo("Not Found"); + assertThat(httpException.isPlainText()).isTrue(); + assertThat(httpException.contentType()) + .isPresent() + .get().isEqualTo("text/plain"); } @Test @@ -798,6 +804,20 @@ void async_list_as() throws ExecutionException, InterruptedException { assertThat(helloRes).isSameAs(ref.get()); } + @Test + void get_bean_404() { + try { + clientContext.request() + .path("does-not-exist") + .GET() + .bean(HelloDto.class); + } catch (HttpException e) { + assertThat(e.statusCode()).isEqualTo(404); + assertThat(e.contentType()).isPresent().get().isEqualTo("text/plain"); + assertThat(e.bodyAsString()).isEqualTo("Not Found"); + } + } + @Test void get_withPathParamAndQueryParam_returningBean() { @@ -1243,6 +1263,12 @@ void postForm_asBytes_validation_expect_badRequest_extractError() { assertNotNull(httpResponse); assertEquals(422, httpResponse.statusCode()); + assertThat(e.contentType()) + .isPresent() + .get().isEqualTo("application/json"); + + assertThat(e.isPlainText()).isFalse(); + final ErrorResponse errorResponse = e.bean(ErrorResponse.class); assertThat(errorResponse.get("url")).isEqualTo("must be a valid URL"); assertThat(errorResponse.get("name")).isEqualTo("must not be null"); diff --git a/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/MappedException.java b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/MappedException.java index 448ab6352..387c63ce6 100644 --- a/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/MappedException.java +++ b/http-generator-client/src/test/java/io/avaje/http/generator/client/clients/MappedException.java @@ -4,5 +4,35 @@ public class MappedException extends RuntimeException { - public MappedException(HttpException e) {} + private final int statusCode; + private String responseMessage; + private MyJsonErrorPayload myPayload; + + public MappedException(HttpException httpException) { + super("Error response with statusCode: " + httpException.statusCode()); + this.statusCode = httpException.statusCode(); + if (httpException.isPlainText()) { + this.responseMessage = httpException.bodyAsString(); + } else { + this.myPayload = httpException.bean(MyJsonErrorPayload.class); + } + addSuppressed(httpException); + } + + public int statusCode() { + return statusCode; + } + + public String responseMessage() { + return responseMessage; + } + + public MyJsonErrorPayload myPayload() { + return myPayload; + } + + // @Json + static class MyJsonErrorPayload { + // fields + } }