From 3a42aa83bc163e35afe82151da22c00b94d7e481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 17 Sep 2021 15:55:22 +0200 Subject: [PATCH 1/6] Fix invalid assertions in vert.x route tests We were asserting that the error page contains the string "null", in order to check that an NPE is reported. But the error page contains javascript code, and that code contains the javascript keyword "null". So those assertions were always passing... In some cases they were passing even though there was no NPE; the assertion was incorrect and we didn't notice that because it was passing anyway. --- .../io/quarkus/vertx/web/cs/CompletionStageRouteTest.java | 2 +- .../java/io/quarkus/vertx/web/mutiny/JsonMultiRouteTest.java | 4 ++-- .../test/java/io/quarkus/vertx/web/mutiny/MultiRouteTest.java | 4 ++-- .../io/quarkus/vertx/web/mutiny/NdjsonMultiRouteTest.java | 4 ++-- .../java/io/quarkus/vertx/web/mutiny/SSEMultiRouteTest.java | 4 ++-- .../test/java/io/quarkus/vertx/web/mutiny/UniRouteTest.java | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java index 0d8fc91cecd39..c00ff27b26454 100644 --- a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java @@ -44,7 +44,7 @@ public void testCsRoute() { when().get("/failure").then().statusCode(500).body(containsString("boom")); when().get("/sync-failure").then().statusCode(500).body(containsString("boom")); - when().get("/null").then().statusCode(500).body(containsString("null")); + when().get("/null").then().statusCode(500).body(containsString(NullPointerException.class.getName())); when().get("/cs-null").then().statusCode(500); when().get("/void").then().statusCode(204).body(hasLength(0)); } diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/JsonMultiRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/JsonMultiRouteTest.java index ff9b2aec24cac..46f22dece90d2 100644 --- a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/JsonMultiRouteTest.java +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/JsonMultiRouteTest.java @@ -59,8 +59,8 @@ public void testMultiRoute() { .header("content-type", "application/json;charset=utf-8"); when().get("/failure").then().statusCode(500).body(containsString("boom")); - when().get("/null").then().statusCode(500).body(containsString("null")); - when().get("/sync-failure").then().statusCode(500).body(containsString("null")); + when().get("/null").then().statusCode(500).body(containsString(NullPointerException.class.getName())); + when().get("/sync-failure").then().statusCode(500).body(containsString("boom")); } diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/MultiRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/MultiRouteTest.java index 551f395597c5c..d870a6e45aeb3 100644 --- a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/MultiRouteTest.java +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/MultiRouteTest.java @@ -52,8 +52,8 @@ public void testMultiRoute() { .header("content-type", is(nullValue())); when().get("/failure").then().statusCode(500).body(containsString("boom")); - when().get("/null").then().statusCode(500).body(containsString("null")); - when().get("/sync-failure").then().statusCode(500).body(containsString("null")); + when().get("/null").then().statusCode(500).body(containsString(NullPointerException.class.getName())); + when().get("/sync-failure").then().statusCode(500).body(containsString("boom")); } diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/NdjsonMultiRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/NdjsonMultiRouteTest.java index 8c2d652896705..febc1f021494f 100644 --- a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/NdjsonMultiRouteTest.java +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/NdjsonMultiRouteTest.java @@ -80,8 +80,8 @@ public void testNdjsonMultiRoute() { .header(HttpHeaders.CONTENT_TYPE.toString(), CONTENT_TYPE_STREAM_JSON); when().get("/failure").then().statusCode(500).body(containsString("boom")); - when().get("/null").then().statusCode(500).body(containsString("null")); - when().get("/sync-failure").then().statusCode(500).body(containsString("null")); + when().get("/null").then().statusCode(500).body(containsString(NullPointerException.class.getName())); + when().get("/sync-failure").then().statusCode(500).body(containsString("boom")); } static class SimpleBean { diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/SSEMultiRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/SSEMultiRouteTest.java index 3dd1656631f1e..9cfdbb2ea6195 100644 --- a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/SSEMultiRouteTest.java +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/SSEMultiRouteTest.java @@ -106,8 +106,8 @@ public void testSSEMultiRoute() { .header("content-type", is("text/event-stream")); when().get("/failure").then().statusCode(500).body(containsString("boom")); - when().get("/null").then().statusCode(500).body(containsString("null")); - when().get("/sync-failure").then().statusCode(500).body(containsString("null")); + when().get("/null").then().statusCode(500).body(containsString(NullPointerException.class.getName())); + when().get("/sync-failure").then().statusCode(500).body(containsString("boom")); } diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/UniRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/UniRouteTest.java index 87e52fb81b31b..5b4819f433384 100644 --- a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/UniRouteTest.java +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/mutiny/UniRouteTest.java @@ -43,7 +43,7 @@ public void testUniRoute() { when().get("/failure").then().statusCode(500).body(containsString("boom")); when().get("/sync-failure").then().statusCode(500).body(containsString("boom")); - when().get("/null").then().statusCode(500).body(containsString("null")); + when().get("/null").then().statusCode(500).body(containsString(NullPointerException.class.getName())); when().get("/uni-null").then().statusCode(500); when().get("/void").then().statusCode(204).body(hasLength(0)); } From f94b983b7ffcdefdbfe1afc37529a83addeeb7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Tue, 21 Sep 2021 08:46:18 +0200 Subject: [PATCH 2/6] Use consistent phrasing for HTTP 500 error message with or without stack trace Otherwise it's hard to write tests that work in both native mode (prod profile) and non-native mode (dev/test profile). --- .../quarkus/undertow/test/ErrorServletTestCase.java | 2 +- .../quarkus/undertow/runtime/QuarkusErrorServlet.java | 2 +- .../vertx/http/runtime/QuarkusErrorHandler.java | 11 +++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/ErrorServletTestCase.java b/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/ErrorServletTestCase.java index a3c60ded2fb70..4474d8712a1a5 100644 --- a/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/ErrorServletTestCase.java +++ b/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/ErrorServletTestCase.java @@ -31,7 +31,7 @@ public void testJsonError() { RestAssured.given().accept(ContentType.JSON) .when().get("/error").then() .statusCode(500) - .body("details", startsWith("Error handling")) + .body("details", startsWith("Error id")) .body("stack", startsWith("java.lang.RuntimeException: Error !!!")); } } diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java index 5e2046ef91e13..9515d858312f2 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusErrorServlet.java @@ -65,7 +65,7 @@ private static String generateStackTrace(final Throwable exception) { } private static String generateHeaderMessage(final Throwable exception, String uuid) { - return String.format("Error handling %s, %s: %s", uuid, exception.getClass().getName(), + return String.format("Error id %s, %s: %s", uuid, exception.getClass().getName(), extractFirstLine(exception.getMessage())); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index 6484de2a3f34d..94d0849cb54e3 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -92,15 +92,14 @@ public void accept(Throwable throwable) { } String uuid = BASE_ID + ERROR_COUNT.incrementAndGet(); - String details = ""; + String details; String stack = ""; Throwable exception = event.failure(); if (showStack && exception != null) { details = generateHeaderMessage(exception, uuid); stack = generateStackTrace(exception); - } else { - details += "Error id " + uuid; + details = generateHeaderMessage(uuid); } if (event.failure() instanceof IOException) { log.debugf(exception, @@ -154,10 +153,14 @@ private static String generateStackTrace(final Throwable exception) { } private static String generateHeaderMessage(final Throwable exception, String uuid) { - return String.format("Error handling %s, %s: %s", uuid, exception.getClass().getName(), + return String.format("Error id %s, %s: %s", uuid, exception.getClass().getName(), extractFirstLine(exception.getMessage())); } + private static String generateHeaderMessage(String uuid) { + return String.format("Error id %s", uuid); + } + private static String extractFirstLine(final String message) { if (null == message) { return ""; From 91370d1edf78d1cde9e1c5d39927ebd1111cdd01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= Date: Fri, 17 Sep 2021 09:18:20 +0200 Subject: [PATCH 3/6] Rework "Accept" header handling for HTTP 500 response body on unhandled exceptions --- .../vertx/http/UnhandledExceptionTest.java | 264 ++++++++++++++++++ .../http/runtime/QuarkusErrorHandler.java | 101 +++++-- 2 files changed, 346 insertions(+), 19 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java new file mode 100644 index 0000000000000..4a43f13493124 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java @@ -0,0 +1,264 @@ +package io.quarkus.vertx.http; + +import static io.restassured.RestAssured.given; +import static io.restassured.config.HeaderConfig.headerConfig; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; + +import org.hamcrest.Matcher; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; +import io.restassured.config.RestAssuredConfig; +import io.vertx.core.Handler; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; + +public class UnhandledExceptionTest { + + private static final String APPLICATION_JSON = "application/json"; + private static final String TEXT_JSON = "text/json"; + private static final String TEXT_HTML = "text/html"; + private static final String APPLICATION_XML = "application/xml"; + private static final String TEXT_XML = "text/xml"; + private static final String APPLICATION_XHTML = "application/xhtml+xml"; + private static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(BeanRegisteringRouteThatThrowsException.class)); + + @Test + public void testNoAccept() { + given().get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(APPLICATION_JSON) // Default to JSON + .body(jsonBodyMatcher()); + } + + @Test + public void testAcceptUnsupported() { + given() + .accept(APPLICATION_OCTET_STREAM) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) // Default to HTML + .body(htmlBodyMatcher()); + } + + @Test + public void testAcceptApplicationJson() { + given() + .accept(APPLICATION_JSON) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(APPLICATION_JSON) + .body(jsonBodyMatcher()); + } + + @Test + public void testAcceptTextJson() { + given() + .accept(TEXT_JSON) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_JSON) + .body(jsonBodyMatcher()); + } + + @Test + public void testAcceptTextHtml() { + given() + .accept(TEXT_HTML) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) + .body(htmlBodyMatcher()); + } + + @Test + public void testAcceptTextXml() { + given() + .accept(TEXT_XML) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) // Not quite what they want, but better than nothing + .body(htmlBodyMatcher()); + } + + @Test + public void testAcceptApplicationXml() { + given() + .accept(APPLICATION_XML) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) // Not quite what they want, but better than nothing + .body(htmlBodyMatcher()); + } + + @Test + public void testAcceptXHtml() { + given() + .accept(APPLICATION_XHTML) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) // Not quite what they want, but better than nothing + .body(htmlBodyMatcher()); + } + + @Test + public void testAcceptWildcard() { + given() + .accept("text/*") + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_JSON) + .body(jsonBodyMatcher()); + } + + @Test + public void testAcceptParameter() { + // We don't support accept parameters: they will be ignored. + given() + .accept("text/html;q=0.8") + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) + .body(htmlBodyMatcher()); + } + + @Test + public void testMultipleAcceptHeaders() { + RestAssuredConfig multipleAcceptHeadersConfig = RestAssured.config() + .headerConfig(headerConfig().mergeHeadersWithName("Accept")); + given() + .config(multipleAcceptHeadersConfig) + .accept(APPLICATION_JSON) + .accept(TEXT_HTML) + .accept(APPLICATION_OCTET_STREAM) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(APPLICATION_JSON) + .body(jsonBodyMatcher()); + + given() + .config(multipleAcceptHeadersConfig) + .accept(TEXT_HTML) + .accept(APPLICATION_JSON) + .accept(APPLICATION_OCTET_STREAM) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) + .body(htmlBodyMatcher()); + + given() + .config(multipleAcceptHeadersConfig) + .accept(APPLICATION_OCTET_STREAM) + .accept(TEXT_HTML) + .accept(APPLICATION_JSON) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) + .body(htmlBodyMatcher()); + + given() + .config(multipleAcceptHeadersConfig) + .accept(APPLICATION_OCTET_STREAM) + .accept(APPLICATION_JSON) + .accept(TEXT_HTML) + .get("/unhandled-exception") + .then() + .statusCode(500) + // Ideally we'd like APPLICATION_JSON here, but due to some strange behavior of + // io.vertx.ext.web.ParsedHeaderValues.findBestUserAcceptedIn, + // we get this. + .contentType(TEXT_HTML) + .body(htmlBodyMatcher()); + } + + @Test + public void testCompositeAcceptHeaders() { + given() + .accept(APPLICATION_JSON + ", " + TEXT_HTML + ", " + APPLICATION_OCTET_STREAM) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(APPLICATION_JSON) + .body(jsonBodyMatcher()); + + given() + .accept(TEXT_HTML + ", " + APPLICATION_JSON + ", " + APPLICATION_OCTET_STREAM) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) + .body(htmlBodyMatcher()); + + given() + .accept(APPLICATION_OCTET_STREAM + ", " + TEXT_HTML + ", " + APPLICATION_JSON) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(TEXT_HTML) + .body(htmlBodyMatcher()); + + given() + .accept(APPLICATION_OCTET_STREAM + ", " + APPLICATION_JSON + ", " + TEXT_HTML) + .get("/unhandled-exception") + .then() + .statusCode(500) + .contentType(APPLICATION_JSON) + .body(jsonBodyMatcher()); + } + + private Matcher jsonBodyMatcher() { + return allOf( + containsString("\"details\":\"Error id"), + containsString("\"stack\":\"java.lang.RuntimeException: Simulated failure"), + containsString("at " + BeanRegisteringRouteThatThrowsException.class.getName() + "$1.handle")); + } + + private Matcher htmlBodyMatcher() { + return allOf( + containsString(""), + containsString("Internal Server Error"), + containsString("java.lang.RuntimeException: Simulated failure"), + containsString("at " + BeanRegisteringRouteThatThrowsException.class.getName() + "$1.handle")); + } + + @ApplicationScoped + static class BeanRegisteringRouteThatThrowsException { + + public void register(@Observes Router router) { + router.route("/unhandled-exception").handler(new Handler<RoutingContext>() { + @Override + public void handle(RoutingContext event) { + throw new RuntimeException("Simulated failure"); + } + }); + } + + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index 94d0849cb54e3..c488bbe2c6753 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -5,6 +5,9 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; @@ -19,7 +22,9 @@ import io.quarkus.security.UnauthorizedException; import io.quarkus.vertx.http.runtime.security.HttpAuthenticator; import io.vertx.core.Handler; +import io.vertx.ext.web.MIMEHeader; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.impl.ParsableMIMEValue; public class QuarkusErrorHandler implements Handler<RoutingContext> { @@ -95,6 +100,16 @@ public void accept(Throwable throwable) { String details; String stack = ""; Throwable exception = event.failure(); + String responseContentType = null; + try { + responseContentType = ContentTypes.pickFirstSupportedAndAcceptedContentType(event); + } catch (RuntimeException e) { + // Let's shield ourselves from bugs in this parsing code: + // we're already handling an exception, + // so the priority is to return *something* to the user. + // If we can't pick the appropriate content-type, well, so be it. + exception.addSuppressed(e); + } if (showStack && exception != null) { details = generateHeaderMessage(exception, uuid); stack = generateStackTrace(exception); @@ -117,26 +132,47 @@ public void accept(Throwable throwable) { event.response().end(); return; } - String accept = event.request().getHeader("Accept"); - if (accept != null && accept.contains("application/json")) { - event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); - String escapedDetails = escapeJsonString(details); - String escapedStack = escapeJsonString(stack); - StringBuilder jsonPayload = new StringBuilder("{\"details\":\"") - .append(escapedDetails) - .append("\",\"stack\":\"") - .append(escapedStack) - .append("\"}"); - writeResponse(event, jsonPayload.toString()); - } else { - //We default to HTML representation - event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); - final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details); - if (showStack && exception != null) { - htmlBuilder.stack(exception); - } - writeResponse(event, htmlBuilder.toString()); + + if (responseContentType == null) { + responseContentType = ""; } + switch (responseContentType) { + case ContentTypes.TEXT_HTML: + case ContentTypes.APPLICATION_XHTML: + case ContentTypes.APPLICATION_XML: + case ContentTypes.TEXT_XML: + htmlResponse(event, details, exception); + break; + case ContentTypes.APPLICATION_JSON: + case ContentTypes.TEXT_JSON: + jsonResponse(event, responseContentType, details, stack); + break; + // We default to HTML representation + default: + htmlResponse(event, details, exception); + break; + } + } + + private void jsonResponse(RoutingContext event, String contentType, String details, String stack) { + event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=utf-8"); + String escapedDetails = escapeJsonString(details); + String escapedStack = escapeJsonString(stack); + StringBuilder jsonPayload = new StringBuilder("{\"details\":\"") + .append(escapedDetails) + .append("\",\"stack\":\"") + .append(escapedStack) + .append("\"}"); + writeResponse(event, jsonPayload.toString()); + } + + private void htmlResponse(RoutingContext event, String details, Throwable exception) { + event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); + final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details); + if (showStack && exception != null) { + htmlBuilder.stack(exception); + } + writeResponse(event, htmlBuilder.toString()); } private void writeResponse(RoutingContext event, String output) { @@ -203,4 +239,31 @@ static String escapeJsonString(final String text) { return sb.toString(); } + private static final class ContentTypes { + + private ContentTypes() { + } + + private static final String APPLICATION_JSON = "application/json"; + private static final String TEXT_JSON = "text/json"; + private static final String TEXT_HTML = "text/html"; + private static final String APPLICATION_XHTML = "application/xhtml+xml"; + private static final String APPLICATION_XML = "application/xml"; + private static final String TEXT_XML = "text/xml"; + + // WARNING: The order matters for wildcards: if text/json is before text/html, then text/* will match text/json. + private static final Collection<MIMEHeader> SUPPORTED = Arrays.asList( + new ParsableMIMEValue(APPLICATION_JSON).forceParse(), + new ParsableMIMEValue(TEXT_JSON).forceParse(), + new ParsableMIMEValue(TEXT_HTML).forceParse(), + new ParsableMIMEValue(APPLICATION_XHTML).forceParse(), + new ParsableMIMEValue(APPLICATION_XML).forceParse(), + new ParsableMIMEValue(TEXT_XML).forceParse()); + + static String pickFirstSupportedAndAcceptedContentType(RoutingContext context) { + List<MIMEHeader> acceptableTypes = context.parsedHeaders().accept(); + MIMEHeader result = context.parsedHeaders().findBestUserAcceptedIn(acceptableTypes, SUPPORTED); + return result == null ? null : result.value(); + } + } } From 77a160d00afa23de5d496dabba5e0c167b8cbfce Mon Sep 17 00:00:00 2001 From: Dmitri Bourlatchkov <dmitri.bourlatchkov@dremio.com> Date: Fri, 17 Sep 2021 15:06:33 -0400 Subject: [PATCH 4/6] Add HTTP config parameter to control the content type of unexpected error responses Applications do not always control the context of unexpected exceptions and may not be able to provide exception mappers in all cases. QuarkusErrorHandler previously replied on the `Accept` HTTP header to decide whether to format the error response as JSON or HTML. RFC6839 defines the `+json` media type suffix that may be used by applications to create custom media types compatible with the JSON format. For example: `application/my-app+json` In those cases QuarkusErrorHandler cannot reasonably be able to produce the exact expected response contents type because its specification is application-dependent. However, it may still be preferable to return a generic `applicaiton/json` response as opposed to HTML in those cases. The new (optional) configuration setting allows applications to request a specific contents type for formatting unexpected error responses. The default Quarkus behaviour remains unchanged. QuarkusErrorHandler will use the `Accept` header for determining the response contents type. --- extensions/vertx-http/runtime/pom.xml | 10 ++++ .../vertx/http/runtime/HttpConfiguration.java | 15 +++++ .../http/runtime/QuarkusErrorHandler.java | 16 ++++- .../vertx/http/runtime/VertxHttpRecorder.java | 3 +- .../http/runtime/QuarkusErrorHandlerTest.java | 59 +++++++++++++++++++ 5 files changed, 101 insertions(+), 2 deletions(-) diff --git a/extensions/vertx-http/runtime/pom.xml b/extensions/vertx-http/runtime/pom.xml index a7ab8fd5933b6..3b6ab58554b66 100644 --- a/extensions/vertx-http/runtime/pom.xml +++ b/extensions/vertx-http/runtime/pom.xml @@ -78,6 +78,16 @@ <artifactId>jackson-databind</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-junit-jupiter</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index cd2c3b962eb9d..5a674e387624c 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -231,6 +231,16 @@ public class HttpConfiguration { @ConfigItem public boolean enableDecompression; + /** + * Provides a hint (optional) for the contents type of responses generated for + * the errors not handled by the application. + * <p> + * When unset, Quarkus will decide which contents type to use based on request headers. + * </p> + */ + @ConfigItem + public Optional<PayloadHint> unhandledErrorContentsType; + public ProxyConfig proxy; public int determinePort(LaunchMode launchMode) { @@ -246,4 +256,9 @@ public enum InsecureRequests { REDIRECT, DISABLED; } + + public enum PayloadHint { + JSON, + HTML, + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index c488bbe2c6753..de7b78350be60 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -8,6 +8,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; @@ -39,9 +40,11 @@ public class QuarkusErrorHandler implements Handler<RoutingContext> { private static final AtomicLong ERROR_COUNT = new AtomicLong(); private final boolean showStack; + private final Optional<HttpConfiguration.PayloadHint> contentTypeHint; - public QuarkusErrorHandler(boolean showStack) { + public QuarkusErrorHandler(boolean showStack, Optional<HttpConfiguration.PayloadHint> contentTypeHint) { this.showStack = showStack; + this.contentTypeHint = contentTypeHint; } @Override @@ -136,6 +139,17 @@ public void accept(Throwable throwable) { if (responseContentType == null) { responseContentType = ""; } + if (contentTypeHint.isPresent()) { + switch (contentTypeHint.get()) { + case JSON: + jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack); + break; + case HTML: + htmlResponse(event, details, exception); + break; + } + return; + } switch (responseContentType) { case ContentTypes.TEXT_HTML: case ContentTypes.APPLICATION_XHTML: diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index a7cf733fbb442..5376e870eb2b1 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -308,7 +308,8 @@ public void finalizeRouter(BeanContainer container, Consumer<Route> defaultRoute defaultRouteHandler.accept(httpRouteRouter.route().order(DEFAULT_ROUTE_ORDER)); } - httpRouteRouter.route().last().failureHandler(new QuarkusErrorHandler(launchMode.isDevOrTest())); + httpRouteRouter.route().last().failureHandler( + new QuarkusErrorHandler(launchMode.isDevOrTest(), httpConfiguration.unhandledErrorContentsType)); if (requireBodyHandler) { //if this is set then everything needs the body handler installed diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java index 721cde5b029f1..1a37d1539c8a0 100644 --- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java @@ -1,13 +1,30 @@ package io.quarkus.vertx.http.runtime; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; + +import java.util.Optional; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import io.netty.handler.codec.http.HttpHeaderNames; import io.vertx.core.json.Json; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.impl.ParsableMIMEValue; +@ExtendWith(MockitoExtension.class) class QuarkusErrorHandlerTest { + private static final Throwable testError = new IllegalStateException("test123"); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + RoutingContext routingContext; + @Test public void string_with_tab_should_be_correctly_escaped() { String initial = "String with a tab\tcharacter"; @@ -32,4 +49,46 @@ public void string_with_quotes_should_be_correctly_escaped() { assertEquals(initial, parsed); } + @Test + public void json_content_type_hint_should_be_respected() { + QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.of(HttpConfiguration.PayloadHint.JSON)); + Mockito.when(routingContext.failure()).thenReturn(testError); + Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) + .thenReturn(new ParsableMIMEValue("text/html").forceParse()); + errorHandler.handle(routingContext); + Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, + "application/json; charset=utf-8"); + } + + @Test + public void content_type_should_default_to_json_if_accepted() { + QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.empty()); + Mockito.when(routingContext.failure()).thenReturn(testError); + Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) + .thenReturn(new ParsableMIMEValue("application/json").forceParse()); + errorHandler.handle(routingContext); + Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, + "application/json; charset=utf-8"); + } + + @Test + public void html_content_type_hint_should_be_respected() { + QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.of(HttpConfiguration.PayloadHint.HTML)); + Mockito.when(routingContext.failure()).thenReturn(testError); + Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) + .thenReturn(new ParsableMIMEValue("application/json").forceParse()); + errorHandler.handle(routingContext); + Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); + } + + @Test + public void content_type_should_default_to_html_if_accepted() { + QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.empty()); + Mockito.when(routingContext.failure()).thenReturn(testError); + Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) + .thenReturn(new ParsableMIMEValue("text/html").forceParse()); + errorHandler.handle(routingContext); + Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, + "text/html; charset=utf-8"); + } } From 02b30c8d3c7000def14073ad87600a9836edccd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= <yoann@hibernate.org> Date: Mon, 20 Sep 2021 09:18:22 +0200 Subject: [PATCH 5/6] Consider the configured unhandled exception content type as a default ... rather than an override. --- .../vertx/http/runtime/HttpConfiguration.java | 10 +++++-- .../http/runtime/QuarkusErrorHandler.java | 28 ++++++++--------- .../vertx/http/runtime/VertxHttpRecorder.java | 2 +- .../http/runtime/QuarkusErrorHandlerTest.java | 30 ++++++++++++++++--- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 5a674e387624c..e957143cd6ee8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -232,14 +232,18 @@ public class HttpConfiguration { public boolean enableDecompression; /** - * Provides a hint (optional) for the contents type of responses generated for + * Provides a hint (optional) for the default content type of responses generated for * the errors not handled by the application. * <p> - * When unset, Quarkus will decide which contents type to use based on request headers. + * If the client requested a supported content-type in request headers + * (e.g. "Accept: application/json", "Accept: text/html"), + * Quarkus will use that content type. + * <p> + * Otherwise, it will default to the content type configured here. * </p> */ @ConfigItem - public Optional<PayloadHint> unhandledErrorContentsType; + public Optional<PayloadHint> unhandledErrorContentTypeDefault; public ProxyConfig proxy; diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index de7b78350be60..bb7b76d586dc2 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -40,11 +40,11 @@ public class QuarkusErrorHandler implements Handler<RoutingContext> { private static final AtomicLong ERROR_COUNT = new AtomicLong(); private final boolean showStack; - private final Optional<HttpConfiguration.PayloadHint> contentTypeHint; + private final Optional<HttpConfiguration.PayloadHint> contentTypeDefault; - public QuarkusErrorHandler(boolean showStack, Optional<HttpConfiguration.PayloadHint> contentTypeHint) { + public QuarkusErrorHandler(boolean showStack, Optional<HttpConfiguration.PayloadHint> contentTypeDefault) { this.showStack = showStack; - this.contentTypeHint = contentTypeHint; + this.contentTypeDefault = contentTypeDefault; } @Override @@ -139,17 +139,6 @@ public void accept(Throwable throwable) { if (responseContentType == null) { responseContentType = ""; } - if (contentTypeHint.isPresent()) { - switch (contentTypeHint.get()) { - case JSON: - jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack); - break; - case HTML: - htmlResponse(event, details, exception); - break; - } - return; - } switch (responseContentType) { case ContentTypes.TEXT_HTML: case ContentTypes.APPLICATION_XHTML: @@ -161,9 +150,16 @@ public void accept(Throwable throwable) { case ContentTypes.TEXT_JSON: jsonResponse(event, responseContentType, details, stack); break; - // We default to HTML representation default: - htmlResponse(event, details, exception); + // We default to HTML representation + switch (contentTypeDefault.orElse(HttpConfiguration.PayloadHint.HTML)) { + case JSON: + jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack); + break; + case HTML: + htmlResponse(event, details, exception); + break; + } break; } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 5376e870eb2b1..4f351e10fe299 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -309,7 +309,7 @@ public void finalizeRouter(BeanContainer container, Consumer<Route> defaultRoute } httpRouteRouter.route().last().failureHandler( - new QuarkusErrorHandler(launchMode.isDevOrTest(), httpConfiguration.unhandledErrorContentsType)); + new QuarkusErrorHandler(launchMode.isDevOrTest(), httpConfiguration.unhandledErrorContentTypeDefault)); if (requireBodyHandler) { //if this is set then everything needs the body handler installed diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java index 1a37d1539c8a0..f1634fee29f5f 100644 --- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandlerTest.java @@ -50,16 +50,27 @@ public void string_with_quotes_should_be_correctly_escaped() { } @Test - public void json_content_type_hint_should_be_respected() { + public void json_content_type_hint_should_be_respected_if_not_accepted() { QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.of(HttpConfiguration.PayloadHint.JSON)); Mockito.when(routingContext.failure()).thenReturn(testError); Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) - .thenReturn(new ParsableMIMEValue("text/html").forceParse()); + .thenReturn(new ParsableMIMEValue("application/foo+json").forceParse()); errorHandler.handle(routingContext); Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=utf-8"); } + @Test + public void json_content_type_hint_should_be_ignored_if_accepted() { + QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.of(HttpConfiguration.PayloadHint.JSON)); + Mockito.when(routingContext.failure()).thenReturn(testError); + Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) + .thenReturn(new ParsableMIMEValue("text/html").forceParse()); + errorHandler.handle(routingContext); + Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, + "text/html; charset=utf-8"); + } + @Test public void content_type_should_default_to_json_if_accepted() { QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.empty()); @@ -72,15 +83,26 @@ public void content_type_should_default_to_json_if_accepted() { } @Test - public void html_content_type_hint_should_be_respected() { + public void html_content_type_hint_should_be_respected_if_not_accepted() { QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.of(HttpConfiguration.PayloadHint.HTML)); Mockito.when(routingContext.failure()).thenReturn(testError); Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) - .thenReturn(new ParsableMIMEValue("application/json").forceParse()); + .thenReturn(new ParsableMIMEValue("application/foo+json").forceParse()); errorHandler.handle(routingContext); Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); } + @Test + public void html_content_type_hint_should_be_ignored_if_accepted() { + QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.of(HttpConfiguration.PayloadHint.HTML)); + Mockito.when(routingContext.failure()).thenReturn(testError); + Mockito.when(routingContext.parsedHeaders().findBestUserAcceptedIn(any(), any())) + .thenReturn(new ParsableMIMEValue("application/json").forceParse()); + errorHandler.handle(routingContext); + Mockito.verify(routingContext.response().headers()).set(HttpHeaderNames.CONTENT_TYPE, + "application/json; charset=utf-8"); + } + @Test public void content_type_should_default_to_html_if_accepted() { QuarkusErrorHandler errorHandler = new QuarkusErrorHandler(false, Optional.empty()); From e49ae0109369b099836b10093ba3b6e21f97a9b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yoann=20Rodi=C3=A8re?= <yoann@hibernate.org> Date: Mon, 20 Sep 2021 14:43:57 +0200 Subject: [PATCH 6/6] Change the default format of responses returned by QuarkusErrorHandler --- .../vertx/http/UnhandledExceptionTest.java | 18 +++++++++--------- .../http/runtime/QuarkusErrorHandler.java | 11 ++++++----- .../HibernateValidatorFunctionalityTest.java | 2 +- .../HibernateValidatorFunctionalityTest.java | 6 +++--- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java index 4a43f13493124..623efb2fed0f5 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/UnhandledExceptionTest.java @@ -52,8 +52,8 @@ public void testAcceptUnsupported() { .get("/unhandled-exception") .then() .statusCode(500) - .contentType(TEXT_HTML) // Default to HTML - .body(htmlBodyMatcher()); + .contentType(APPLICATION_JSON) // Default to JSON + .body(jsonBodyMatcher()); } @Test @@ -179,8 +179,11 @@ public void testMultipleAcceptHeaders() { .get("/unhandled-exception") .then() .statusCode(500) - .contentType(TEXT_HTML) - .body(htmlBodyMatcher()); + // Ideally we'd like TEXT_HTML here, but due to some strange behavior of + // io.vertx.ext.web.ParsedHeaderValues.findBestUserAcceptedIn, + // we get this. + .contentType(APPLICATION_JSON) + .body(jsonBodyMatcher()); given() .config(multipleAcceptHeadersConfig) @@ -190,11 +193,8 @@ public void testMultipleAcceptHeaders() { .get("/unhandled-exception") .then() .statusCode(500) - // Ideally we'd like APPLICATION_JSON here, but due to some strange behavior of - // io.vertx.ext.web.ParsedHeaderValues.findBestUserAcceptedIn, - // we get this. - .contentType(TEXT_HTML) - .body(htmlBodyMatcher()); + .contentType(APPLICATION_JSON) + .body(jsonBodyMatcher()); } @Test diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index bb7b76d586dc2..b469d594b17ed 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -151,14 +151,15 @@ public void accept(Throwable throwable) { jsonResponse(event, responseContentType, details, stack); break; default: - // We default to HTML representation - switch (contentTypeDefault.orElse(HttpConfiguration.PayloadHint.HTML)) { - case JSON: - jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack); - break; + // We default to JSON representation + switch (contentTypeDefault.orElse(HttpConfiguration.PayloadHint.JSON)) { case HTML: htmlResponse(event, details, exception); break; + case JSON: + default: + jsonResponse(event, ContentTypes.APPLICATION_JSON, details, stack); + break; } break; } diff --git a/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java index b7480e36695cb..8ef836dcb6ded 100644 --- a/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java +++ b/integration-tests/hibernate-validator-resteasy-reactive/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java @@ -126,7 +126,7 @@ public void testRestEndPointReturnValueValidation() { .then() .body(containsString(ResteasyReactiveViolationException.class.getName())) // Exception type .body(containsString("numeric value out of bounds")) // Exception message - .body(containsString("testRestEndPointReturnValueValidation.<return value>")) + .body(containsString("testRestEndPointReturnValueValidation.<return value>")) .body(containsString(HibernateValidatorTestResource.class.getName())) // Stack trace .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); diff --git a/integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java b/integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java index d6def9b5d81be..a5cde57c4284b 100644 --- a/integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java +++ b/integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java @@ -119,7 +119,7 @@ public void testCDIBeanMethodValidationUncaught() { ValidatableResponse response = RestAssured.when() .get("/hibernate-validator/test/cdi-bean-method-validation-uncaught") .then() - .body(containsString("Internal Server Error")) + .body(containsString("Error id")) .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); if (isInternalErrorExceptionLeakedInQuarkusErrorHandlerResponse()) { response @@ -182,13 +182,13 @@ public void testRestEndPointReturnValueValidation() { ValidatableResponse response = RestAssured.when() .get("/hibernate-validator/test/rest-end-point-return-value-validation/plop/") .then() - .body(containsString("Internal Server Error")) + .body(containsString("Error id")) .statusCode(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); if (isInternalErrorExceptionLeakedInQuarkusErrorHandlerResponse()) { response .body(containsString(ResteasyViolationExceptionImpl.class.getName())) // Exception type .body(containsString("numeric value out of bounds")) // Exception message - .body(containsString("testRestEndPointReturnValueValidation.<return value>")) + .body(containsString("testRestEndPointReturnValueValidation.<return value>")) .body(containsString(HibernateValidatorTestResource.class.getName())); // Stack trace }