From e6ac16cfd47e86c001ec1f2a0e7725845498ffdc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 14:53:30 +0200 Subject: [PATCH 01/29] feat: Improved HTML Error pages If the request accepts an HTML Response, respond a nice error page. Allow multiple body assertions in tck. --- http-server-netty/build.gradle | 5 + ...aultHtmlBodyErrorResponseProviderTest.java | 78 ++++++ .../src/test/resources/logback.xml | 1 + .../tests/exceptions/HtmlErrorPageTest.java | 79 ++++++ http-server/build.gradle | 15 ++ .../response/BodyErrorResponseProvider.java | 34 +++ .../DefaultErrorResponseProcessor.java | 58 +++++ .../DefaultHtmlBodyErrorResponseProvider.java | 240 ++++++++++++++++++ .../DefaultJsonBodyErrorResponseProvider.java | 67 +++++ .../response/ErrorResponseProcessor.java | 2 +- .../HateoasErrorResponseProcessor.java | 7 +- .../HtmlBodyErrorResponseProvider.java | 35 +++ .../JsonBodyErrorResponseProvider.java | 34 +++ .../HtmlBodyErrorResponseProviderTest.java | 109 ++++++++ .../io/micronaut/http/tck/AssertionUtils.java | 34 +-- .../io/micronaut/http/tck/BodyAssertion.java | 4 +- .../http/tck/HttpResponseAssertion.java | 27 +- 17 files changed, 792 insertions(+), 37 deletions(-) create mode 100644 http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java create mode 100644 http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/BodyErrorResponseProvider.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonBodyErrorResponseProvider.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProvider.java create mode 100644 http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonBodyErrorResponseProvider.java create mode 100644 http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java diff --git a/http-server-netty/build.gradle b/http-server-netty/build.gradle index e4d2a0d221e..d5a7bf97c12 100644 --- a/http-server-netty/build.gradle +++ b/http-server-netty/build.gradle @@ -105,6 +105,11 @@ dependencies { testImplementation project(":websocket") testImplementation(libs.mimepull) + + testImplementation(libs.micronaut.test.junit5) { + exclude group: 'io.micronaut' + } + testImplementation(libs.junit.jupiter.api) } tasks.withType(Test).configureEach { diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java new file mode 100644 index 00000000000..d23efd01738 --- /dev/null +++ b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java @@ -0,0 +1,78 @@ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.*; +import io.micronaut.http.annotation.Body; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.annotation.Produces; +import io.micronaut.http.annotation.Status; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import io.micronaut.http.client.annotation.Client; +import io.micronaut.http.client.exceptions.HttpClientResponseException; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spock.lang.Specification; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Property(name = "spec.name", value = "DefaultHtmlBodyErrorResponseProviderTest") +@MicronautTest +class DefaultHtmlBodyErrorResponseProviderTest extends Specification { + private static final Logger LOG = LoggerFactory.getLogger(DefaultHtmlBodyErrorResponseProviderTest.class); + + @Inject + HtmlBodyErrorResponseProvider htmlProvider; + + @Client("/") + @Inject + HttpClient httpClient; + + @Test + void validationErrorsShowInHtmlErrorPages() { + BlockingHttpClient client = httpClient.toBlocking(); + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/book/save", new Book("Building Microservices", "", 5000)).accept(MediaType.TEXT_HTML))); + assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus()); + Optional htmlOptional = ex.getResponse().getBody(String.class); + assertTrue(htmlOptional.isPresent()); + String html = htmlOptional.get(); + assertExpectedSubstringInHtml("", html); + assertExpectedSubstringInHtml("book.author: must not be blank", html); + assertExpectedSubstringInHtml("book.pages: must be less than or equal to 4032", html); + } + + private void assertExpectedSubstringInHtml(String expected, String html) { + if (!html.contains(expected)) { + LOG.trace("{}", html); + } + assertTrue(html.contains(expected)); + } + + @Requires(property = "spec.name", value = "DefaultHtmlBodyErrorResponseProviderTest") + @Controller("/book") + static class FooController { + + @Produces(MediaType.TEXT_HTML) + @Post("/save") + @Status(HttpStatus.CREATED) + void save(@Body @Valid Book book) { + } + } + + @Introspected + record Book(@NotBlank String title, @NotBlank String author, @Max(4032 ) int pages) { + } +} diff --git a/http-server-netty/src/test/resources/logback.xml b/http-server-netty/src/test/resources/logback.xml index e315d7867e6..d32bf51310a 100644 --- a/http-server-netty/src/test/resources/logback.xml +++ b/http-server-netty/src/test/resources/logback.xml @@ -24,4 +24,5 @@ + diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java new file mode 100644 index 00000000000..d17700d7bea --- /dev/null +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java @@ -0,0 +1,79 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.tck.tests.exceptions; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.http.HttpHeaders; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.MediaType; +import io.micronaut.http.annotation.*; +import io.micronaut.http.tck.AssertionUtils; +import io.micronaut.http.tck.BodyAssertion; +import io.micronaut.http.tck.HttpResponseAssertion; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Map; + +import static io.micronaut.http.tck.TestScenario.asserts; + +@SuppressWarnings({ + "java:S5960", // We're allowed assertions, as these are used in tests only + "checkstyle:MissingJavadocType", + "checkstyle:DesignForExtension" +}) +public class HtmlErrorPageTest { + private static final String SPEC_NAME = "HtmlErrorPageTest"; + + @Test + void htmlErrorPage() throws IOException { + asserts(SPEC_NAME, + HttpRequest.POST("/book/save", new Book("Building Microservices", "", 5000)).accept(MediaType.TEXT_HTML), + (server, request) -> AssertionUtils.assertThrows( + server, + request, + HttpResponseAssertion.builder() + .status(HttpStatus.BAD_REQUEST) + .body(BodyAssertion.builder().body("").contains()) + .body(BodyAssertion.builder().body("book.author: must not be blank").contains()) + .body(BodyAssertion.builder().body("book.pages: must be less than or equal to 4032").contains()) + .headers(Map.of(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_HTML)) + .build() + ) + ); + } + + @Requires(property = "spec.name", value = "HtmlErrorPageTest") + @Controller("/book") + static class FooController { + + @Produces(MediaType.TEXT_HTML) + @Post("/save") + @Status(HttpStatus.CREATED) + void save(@Body @Valid Book book) { + } + } + + @Introspected + record Book(@NotBlank String title, @NotBlank String author, @Max(4032) int pages) { + + } +} diff --git a/http-server/build.gradle b/http-server/build.gradle index efd70a41a09..2c282eea4a2 100644 --- a/http-server/build.gradle +++ b/http-server/build.gradle @@ -15,6 +15,21 @@ dependencies { annotationProcessor project(":inject-java") testImplementation libs.managed.netty.codec.http + + testAnnotationProcessor(project(":inject-java")) + testAnnotationProcessor platform(libs.test.boms.micronaut.validation) + testAnnotationProcessor (libs.micronaut.validation.processor) { + exclude group: 'io.micronaut' + } + testImplementation platform(libs.test.boms.micronaut.validation) + testImplementation (libs.micronaut.validation) { + exclude group: 'io.micronaut' + } + testImplementation(libs.micronaut.test.junit5) { + exclude group: 'io.micronaut' + } + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.params) } //compileTestGroovy.groovyOptions.forkOptions.jvmArgs = ['-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005'] diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/BodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/BodyErrorResponseProvider.java new file mode 100644 index 00000000000..ad7f7681390 --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/BodyErrorResponseProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.http.HttpResponse; + +/** + * Provides an HTTP Response body of an error response. + * @author Sergio del Amo + * @since 4.7.0 + * @param The body type + */ +public interface BodyErrorResponseProvider { + @NonNull + T body(@NonNull ErrorContext errorContext, @NonNull HttpResponse response); + + @NonNull + String contentType(); + +} diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java new file mode 100644 index 00000000000..3fe1f52f68b --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.context.annotation.Secondary; +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.HttpMethod; +import io.micronaut.http.MediaType; +import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.hateoas.JsonError; +import jakarta.inject.Singleton; + +/** + * Default implementation of {@link ErrorResponseProcessor}. + * It delegates to {@link JsonBodyErrorResponseProvider} for JSON responses and to {@link HtmlBodyErrorResponseProvider} for HTML responses. + * + * @author Sergio del Amo + * @since 4.7.0 + */ +@Internal +@Singleton +@Secondary +final class DefaultErrorResponseProcessor implements ErrorResponseProcessor { + private final JsonBodyErrorResponseProvider jsonBodyErrorResponseProvider; + private final HtmlBodyErrorResponseProvider htmlBodyErrorResponseProvider; + + DefaultErrorResponseProcessor(JsonBodyErrorResponseProvider jsonBodyErrorResponseProvider, HtmlBodyErrorResponseProvider htmlBodyErrorResponseProvider) { + this.jsonBodyErrorResponseProvider = jsonBodyErrorResponseProvider; + this.htmlBodyErrorResponseProvider = htmlBodyErrorResponseProvider; + } + + @Override + public MutableHttpResponse processResponse(ErrorContext errorContext, MutableHttpResponse response) { + if (errorContext.getRequest().getMethod() == HttpMethod.HEAD) { + return (MutableHttpResponse) response; + } + final boolean isError = response.status().getCode() >= 400; + if (errorContext.getRequest().accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE)) && isError) { + return response.body(htmlBodyErrorResponseProvider.body(errorContext, response)) + .contentType(htmlBodyErrorResponseProvider.contentType()); + } + return response.body(jsonBodyErrorResponseProvider.body(errorContext, response)) + .contentType(jsonBodyErrorResponseProvider.contentType()); + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java new file mode 100644 index 00000000000..21067c4e10d --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java @@ -0,0 +1,240 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.context.MessageSource; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.util.LocaleResolver; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.HttpStatus; +import jakarta.inject.Singleton; + +import java.text.MessageFormat; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static io.micronaut.http.HttpStatus.*; +import static io.micronaut.http.HttpStatus.CONNECTION_TIMED_OUT; + +/** + * It generates HTML error response page for a given {@link HttpStatus}. + * @author Sergio del Amo + * @since 4.7.0 + */ +@Internal +@Singleton +final class DefaultHtmlBodyErrorResponseProvider implements HtmlBodyErrorResponseProvider { + private static final Map DEFAULT_ERROR_BOLD = Map.of( + NOT_FOUND, "The page you were looking for doesn’t exist", + REQUEST_ENTITY_TOO_LARGE, "The file or data you are trying to upload exceeds the allowed size", + BAD_GATEWAY, "The server received an invalid response", + SERVICE_UNAVAILABLE, "It may be overloaded, or undergoing scheduled maintenance. Please try again later", + GATEWAY_TIMEOUT, "The service did not respond within the allowed time limit"); + + private static final Map DEFAULT_ERROR = Map.of( + NOT_FOUND, "You may have mistyped the address or the page may have moved", + REQUEST_ENTITY_TOO_LARGE, "Please try again with a smaller file", + BAD_GATEWAY, "Please try again later", + SERVICE_UNAVAILABLE, "The service is temporarily unavailable", + GATEWAY_TIMEOUT, "It may be overloaded, or there may be a temporary technical issue. Please try again later" + ); + + private static final Map SVG; + + static { + Map svgs = new ConcurrentHashMap<>(); + svgs.put(BAD_REQUEST, ""); // 400 + svgs.put(UNAUTHORIZED, ""); // 401 + svgs.put(PAYMENT_REQUIRED, ""); // 402 + svgs.put(FORBIDDEN, ""); // 403 + svgs.put(NOT_FOUND, ""); // 404 + svgs.put(METHOD_NOT_ALLOWED, ""); // 405 + svgs.put(NOT_ACCEPTABLE, ""); // 406 + svgs.put(PROXY_AUTHENTICATION_REQUIRED, ""); // 407 + svgs.put(REQUEST_TIMEOUT, ""); // 408 + svgs.put(CONFLICT, ""); // 409 + svgs.put(GONE, ""); // 410 + svgs.put(LENGTH_REQUIRED, ""); // 411 + svgs.put(PRECONDITION_FAILED, ""); // 412 + svgs.put(REQUEST_ENTITY_TOO_LARGE, ""); // 413 + svgs.put(REQUEST_URI_TOO_LONG, ""); // 414 + svgs.put(UNSUPPORTED_MEDIA_TYPE, ""); // 415 + svgs.put(REQUESTED_RANGE_NOT_SATISFIABLE, ""); // 416 + svgs.put(EXPECTATION_FAILED, ""); // 417 + svgs.put(I_AM_A_TEAPOT, ""); // 418 + svgs.put(ENHANCE_YOUR_CALM, ""); // 420 + svgs.put(MISDIRECTED_REQUEST, ""); // 421 + svgs.put(UNPROCESSABLE_ENTITY, ""); // 422 + svgs.put(LOCKED, ""); // 423 + svgs.put(FAILED_DEPENDENCY, ""); // 424 + svgs.put(TOO_EARLY, ""); // 425 + svgs.put(UPGRADE_REQUIRED, ""); // 426 + svgs.put(PRECONDITION_REQUIRED, ""); // 428 + svgs.put(TOO_MANY_REQUESTS, ""); // 429 + svgs.put(REQUEST_HEADER_FIELDS_TOO_LARGE, ""); // 431 + svgs.put(NO_RESPONSE, ""); // 444 + svgs.put(BLOCKED_BY_WINDOWS_PARENTAL_CONTROLS, ""); // 450 + svgs.put(UNAVAILABLE_FOR_LEGAL_REASONS, ""); // 451 + svgs.put(REQUEST_HEADER_TOO_LARGE, ""); // 494 + svgs.put(INTERNAL_SERVER_ERROR, ""); // 500 + svgs.put(NOT_IMPLEMENTED, ""); // 501 + svgs.put(BAD_GATEWAY, ""); // 502 + svgs.put(SERVICE_UNAVAILABLE, ""); // 503 + svgs.put(GATEWAY_TIMEOUT, ""); // 504 + svgs.put(HTTP_VERSION_NOT_SUPPORTED, ""); // 505 + svgs.put(VARIANT_ALSO_NEGOTIATES, ""); // 506 + svgs.put(INSUFFICIENT_STORAGE, ""); // 507 + svgs.put(LOOP_DETECTED, ""); // 508 + svgs.put(BANDWIDTH_LIMIT_EXCEEDED, ""); // 509 + svgs.put(NOT_EXTENDED, ""); // 510 + svgs.put(NETWORK_AUTHENTICATION_REQUIRED, ""); // 511 + svgs.put(CONNECTION_TIMED_OUT, ""); // 522 + SVG = Collections.unmodifiableMap(svgs); + } + private static final String CSS = """ + *, *::before, *::after { + box-sizing: border-box; + } + * { + margin: 0; + } + html { + font-size: 16px; + } + body { + background: #2559a7; + color: #FFF; + display: grid; + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: clamp(1rem, 2.5vw, 2rem); + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: 400; + letter-spacing: -0.0025em; + line-height: 1.4; + min-height: 100vh; + place-items: center; + text-rendering: optimizeLegibility; + -webkit-text-size-adjust: 100%; + } + a { + color: inherit; + font-weight: 700; + text-decoration: underline; + text-underline-offset: 0.0925em; + } + b, strong { + font-weight: 700; + } + i, em { + font-style: italic; + } + main { + display: grid; + gap: 1em; + padding: 2em; + place-items: center; + text-align: center; + } + main header { + width: min(100%, 18em); + } + main header svg { + height: auto; + max-width: 100%; + width: 100%; + } + main article { + width: min(100%, 30em); + } + main article p { + font-size: 75%; + } + main article br { + display: none; + @media(min-width: 48em) { + display: inline; + } + } + """; + + private final MessageSource messageSource; + private final LocaleResolver> localeResolver; + private final Map cache = new ConcurrentHashMap<>(); + + DefaultHtmlBodyErrorResponseProvider(MessageSource messageSource, + LocaleResolver> localeResolver) { + this.messageSource = messageSource; + this.localeResolver = localeResolver; + } + + @Override + public String body(@NonNull ErrorContext errorContext, @NonNull HttpResponse response) { + final HttpStatus status = response.status(); + Locale locale = localeResolver.resolveOrDefault(errorContext.getRequest()); + return cache.computeIfAbsent(new LocaleStatus(locale, status), key -> html(locale, status, errorContext)); + } + + private String html(Locale locale, HttpStatus status, ErrorContext errorContext) { + final String errorTitleCode = status + ".error.title"; + return MessageFormat.format("{0} — {1}
{3}
{4}
", + status.getCode(), + messageSource.getMessage(errorTitleCode, status.getReason(), locale), + CSS, + SVG.get(status), + article(locale, status, errorContext)); + } + + private String article(Locale locale, HttpStatus status, ErrorContext errorContext) { + final String errorBoldCode = status + ".error.bold"; + final String errorCode = status + ".error"; + String defaultErrorBold = DEFAULT_ERROR_BOLD.get(status); + String defaultError = DEFAULT_ERROR.get(status); + String errorBold = defaultErrorBold != null ? messageSource.getMessage(errorBoldCode, defaultErrorBold, locale) : messageSource.getMessage(errorBoldCode, locale).orElse(null); + String error = defaultError != null ? messageSource.getMessage(errorCode, defaultError, locale) : messageSource.getMessage(errorCode, locale).orElse(null); + StringBuilder sb = new StringBuilder(); + + for (io.micronaut.http.server.exceptions.response.Error e : errorContext.getErrors()) { + if (!e.getMessage().equalsIgnoreCase(status.getReason())) { + sb.append(e.getMessage()); + sb.append("
"); + } + } + + if (error != null || errorBold != null) { + sb.append("

"); + if (errorBold != null) { + sb.append(""); + sb.append(errorBold); + sb.append(". "); + } + if (error != null) { + sb.append(error); + sb.append("."); + } + sb.append("

"); + } + return sb.toString(); + } + + private record LocaleStatus(Locale locale, HttpStatus status) { + + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonBodyErrorResponseProvider.java new file mode 100644 index 00000000000..5760dd2371d --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonBodyErrorResponseProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.core.annotation.Internal; +import io.micronaut.http.HttpResponse; +import io.micronaut.http.hateoas.JsonError; +import io.micronaut.http.hateoas.Link; +import io.micronaut.http.hateoas.Resource; +import io.micronaut.json.JsonConfiguration; +import jakarta.inject.Singleton; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of {@link JsonBodyErrorResponseProvider} which returns a {@link JsonError}. + * + * @since 4.7.0 + */ +@Internal +@Singleton +class DefaultJsonBodyErrorResponseProvider implements JsonBodyErrorResponseProvider { + private final boolean alwaysSerializeErrorsAsList; + + DefaultJsonBodyErrorResponseProvider(JsonConfiguration jacksonConfiguration) { + this.alwaysSerializeErrorsAsList = jacksonConfiguration.isAlwaysSerializeErrorsAsList(); + } + + @Override + public JsonError body(ErrorContext errorContext, HttpResponse response) { + JsonError error; + if (!errorContext.hasErrors()) { + error = new JsonError(response.reason()); + } else if (errorContext.getErrors().size() == 1 && !alwaysSerializeErrorsAsList) { + Error jsonError = errorContext.getErrors().get(0); + error = new JsonError(jsonError.getMessage()); + jsonError.getPath().ifPresent(error::path); + } else { + error = new JsonError(response.reason()); + List errors = new ArrayList<>(); + for (Error jsonError : errorContext.getErrors()) { + errors.add(new JsonError(jsonError.getMessage()).path(jsonError.getPath().orElse(null))); + } + error.embedded("errors", errors); + } + try { + error.link(Link.SELF, Link.of(errorContext.getRequest().getUri())); + } catch (IllegalArgumentException ignored) { + // invalid URI, don't include it + } + return error; + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseProcessor.java index 37fd3d27cd7..9c4fa449ee4 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseProcessor.java @@ -26,7 +26,7 @@ * @author James Kleeh * @since 2.4.0 */ -@DefaultImplementation(HateoasErrorResponseProcessor.class) +@DefaultImplementation(DefaultErrorResponseProcessor.class) public interface ErrorResponseProcessor { /** diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java index 007182c9160..4e0ac3579e7 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java @@ -15,7 +15,6 @@ */ package io.micronaut.http.server.exceptions.response; -import io.micronaut.context.annotation.Secondary; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpMethod; import io.micronaut.http.MediaType; @@ -24,8 +23,6 @@ import io.micronaut.http.hateoas.Link; import io.micronaut.http.hateoas.Resource; import io.micronaut.json.JsonConfiguration; -import jakarta.inject.Singleton; - import java.util.ArrayList; import java.util.List; @@ -34,9 +31,9 @@ * * @author James Kleeh * @since 2.4.0 + * @deprecated use {@link io.micronaut.http.server.exceptions.response.DefaultErrorResponseProcessor} instead */ -@Singleton -@Secondary +@Deprecated(forRemoval = true) public class HateoasErrorResponseProcessor implements ErrorResponseProcessor { private final boolean alwaysSerializeErrorsAsList; diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProvider.java new file mode 100644 index 00000000000..4246c1781a4 --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.context.annotation.DefaultImplementation; +import io.micronaut.http.MediaType; + +/** + * A {@link BodyErrorResponseProvider} for HTML responses. + * Responses with content type {@link io.micronaut.http.MediaType#TEXT_HTML}. + * @author Sergio del Amo + * @since 4.7.0 + * @param The body type + */ +@DefaultImplementation(DefaultHtmlBodyErrorResponseProvider.class) +public interface HtmlBodyErrorResponseProvider extends BodyErrorResponseProvider { + + @Override + default String contentType() { + return MediaType.TEXT_HTML; + } +} diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonBodyErrorResponseProvider.java new file mode 100644 index 00000000000..4a72f663876 --- /dev/null +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonBodyErrorResponseProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.http.MediaType; +import jakarta.inject.Singleton; + +/** + * A {@link BodyErrorResponseProvider} for JSON responses. + * Responses with content type {@link io.micronaut.http.MediaType#APPLICATION_JSON}. + * @author Sergio del Amo + * @since 4.7.0 + * @param The error type + */ +@Singleton +public interface JsonBodyErrorResponseProvider extends BodyErrorResponseProvider { + @Override + default String contentType() { + return MediaType.APPLICATION_JSON; + } +} diff --git a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java new file mode 100644 index 00000000000..ef177bd603d --- /dev/null +++ b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java @@ -0,0 +1,109 @@ +package io.micronaut.http.server.exceptions.response; + +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.convert.value.MutableConvertibleValues; +import io.micronaut.http.*; +import io.micronaut.http.simple.SimpleHttpRequest; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import spock.lang.Specification; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@MicronautTest(startApplication = false) +class HtmlBodyErrorResponseProviderTest extends Specification { + private static final Logger LOG = LoggerFactory.getLogger(HtmlBodyErrorResponseProviderTest.class); + + @Inject + HtmlBodyErrorResponseProvider htmlProvider; + + @ParameterizedTest + @EnumSource(HttpStatus.class) + void htmlPageforStatus(HttpStatus status) { + if (status.getCode() >= 400) { + + ErrorContext errorContext = new ErrorContext() { + @Override + public @NonNull HttpRequest getRequest() { + return new SimpleHttpRequest(HttpMethod.GET, "/foobar", null); + } + + @Override + public @NonNull Optional getRootCause() { + return Optional.empty(); + } + + @Override + public @NonNull List getErrors() { + return Collections.emptyList(); + } + }; + HttpResponse response = new HttpResponse() { + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public int code() { + return 0; + } + + @Override + public String reason() { + return ""; + } + + @Override + public HttpHeaders getHeaders() { + return null; + } + + @Override + public MutableConvertibleValues getAttributes() { + return null; + } + + @Override + public Optional getBody() { + return Optional.empty(); + } + }; + String html = htmlProvider.body(errorContext, response); + + assertNotNull(html); + assertExpectedSubstringInHtml(status.getReason(), html); + assertExpectedSubstringInHtml("", html); + if (status.getCode() == 404) { + assertExpectedSubstringInHtml("The page you were looking for doesn’t exist", html); + assertExpectedSubstringInHtml("You may have mistyped the address or the page may have moved", html); + } else if (status.getCode() == 413) { + assertExpectedSubstringInHtml("The file or data you are trying to upload exceeds the allowed size", html); + assertExpectedSubstringInHtml("Please try again with a smaller file", html); + } else if (status.getCode() == 502) { + assertExpectedSubstringInHtml("The server received an invalid response", html); + assertExpectedSubstringInHtml("Please try again later", html); + } else if (status.getCode() == 503) { + assertExpectedSubstringInHtml("It may be overloaded, or undergoing scheduled maintenance. Please try again later", html); + assertExpectedSubstringInHtml("The service is temporarily unavailable", html); + } else if (status.getCode() == 504) { + assertExpectedSubstringInHtml("The service did not respond within the allowed time limit", html); + assertExpectedSubstringInHtml("It may be overloaded, or there may be a temporary technical issue. Please try again later", html); + } + } + } + + private void assertExpectedSubstringInHtml(String expected, String html) { + if (!html.contains(expected)) { + LOG.trace("{}", html); + } + assertTrue(html.contains(expected)); + } +} diff --git a/http-tck/src/main/java/io/micronaut/http/tck/AssertionUtils.java b/http-tck/src/main/java/io/micronaut/http/tck/AssertionUtils.java index 8deac8ccb89..d5b5f0ebeed 100644 --- a/http-tck/src/main/java/io/micronaut/http/tck/AssertionUtils.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/AssertionUtils.java @@ -19,16 +19,14 @@ import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.core.type.Argument; -import io.micronaut.http.HttpHeaders; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpResponse; -import io.micronaut.http.HttpStatus; +import io.micronaut.http.*; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.exceptions.HttpClientResponseException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.api.function.ThrowingSupplier; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiConsumer; @@ -60,7 +58,7 @@ public static void assertThrows(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { Executable e = assertion.getBody() != null ? - () -> server.exchange(request, Argument.of(assertion.getBody().getBodyType()), errorType(assertion)) : + () -> server.exchange(request, Argument.of(assertion.getBody().stream().map(BodyAssertion::getBodyType).findFirst().orElseThrow()), errorType(assertion)) : () -> server.exchange(request); HttpClientResponseException thrown = Assertions.assertThrows(HttpClientResponseException.class, e); HttpResponse response = thrown.getResponse(); @@ -75,11 +73,15 @@ private static Argument errorType(HttpResponseAssertion assertion) { if (assertion.getBody() == null) { return HttpClient.DEFAULT_ERROR_TYPE; } - if (assertion.getBody().getErrorType() == null) { - return HttpClient.DEFAULT_ERROR_TYPE; - } - return Argument.of(assertion.getBody().getErrorType()); - + return assertion.getBody() + .stream() + .map(BodyAssertion::getErrorType) + .findFirst() + .map(Argument::of) + .orElseGet(() -> { + Argument defaultErrorType = HttpClient.DEFAULT_ERROR_TYPE; + return defaultErrorType; + }); } public static void assertThrows(@NonNull ServerUnderTest server, @@ -110,7 +112,7 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, @NonNull HttpRequest request, @NonNull HttpResponseAssertion assertion) { ThrowingSupplier> executable = assertion.getBody() != null ? - () -> server.exchange(request, Argument.of(assertion.getBody().getBodyType()), errorType(assertion)) : + () -> server.exchange(request, Argument.of(assertion.getBody().stream().map(BodyAssertion::getBodyType).findFirst().orElseThrow()), errorType(assertion)) : () -> server.exchange(request); HttpResponse response = Assertions.assertDoesNotThrow(executable); assertEquals(assertion.getHttpStatus(), response.getStatus()); @@ -119,10 +121,12 @@ public static void assertDoesNotThrow(@NonNull ServerUnderTest server, assertion.getResponseConsumer().ifPresent(httpResponseConsumer -> httpResponseConsumer.accept(response)); } - private static void assertBody(@NonNull HttpResponse response, @Nullable BodyAssertion bodyAssertion) { - if (bodyAssertion != null) { - Optional bodyOptional = response.getBody(bodyAssertion.getBodyType()); - bodyAssertion.evaluate(bodyOptional.orElse(null)); + private static void assertBody(@NonNull HttpResponse response, @Nullable List> bodyAssertions) { + if (bodyAssertions != null) { + for (BodyAssertion bodyAssertion : bodyAssertions) { + Optional bodyOptional = response.getBody(bodyAssertion.getBodyType()); + bodyAssertion.evaluate(bodyOptional.orElse(null)); + } } } diff --git a/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java index 9b7aa0cb810..029bf28dce7 100644 --- a/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/BodyAssertion.java @@ -80,8 +80,8 @@ public static BodyAssertion.Builder builder() { * @param body The HTTP Response Body */ @SuppressWarnings("java:S5960") // Assertion is the whole point of this method - public void evaluate(T body) { - assertTrue(this.evaluator.test(expected, body), () -> this.evaluator.message(expected, body)); + public void evaluate(Object body) { + assertTrue(this.evaluator.test(expected, (T) body), () -> this.evaluator.message(expected, (T) body)); } /** diff --git a/http-tck/src/main/java/io/micronaut/http/tck/HttpResponseAssertion.java b/http-tck/src/main/java/io/micronaut/http/tck/HttpResponseAssertion.java index 1ce187026bb..e506ee6cd06 100644 --- a/http-tck/src/main/java/io/micronaut/http/tck/HttpResponseAssertion.java +++ b/http-tck/src/main/java/io/micronaut/http/tck/HttpResponseAssertion.java @@ -21,10 +21,7 @@ import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.function.Consumer; /** @@ -36,18 +33,18 @@ public final class HttpResponseAssertion { private final HttpStatus httpStatus; private final Map headers; - private final BodyAssertion bodyAssertion; + private final List> bodyAssertions; @Nullable private final Consumer> responseConsumer; private HttpResponseAssertion(HttpStatus httpStatus, Map headers, - BodyAssertion bodyAssertion, + List> bodyAssertions, @Nullable Consumer> responseConsumer) { this.httpStatus = httpStatus; this.headers = headers; - this.bodyAssertion = bodyAssertion; + this.bodyAssertions = bodyAssertions; this.responseConsumer = responseConsumer; } @@ -77,8 +74,8 @@ public Map getHeaders() { * @return Expected HTTP Response body */ - public BodyAssertion getBody() { - return bodyAssertion; + public List> getBody() { + return bodyAssertions; } /** @@ -95,7 +92,7 @@ public static HttpResponseAssertion.Builder builder() { public static class Builder { private HttpStatus httpStatus; private Map headers; - private BodyAssertion bodyAssertion; + private List> bodyAssertions; private Consumer> responseConsumer; @@ -139,8 +136,7 @@ public Builder header(String headerName, String headerValue) { * @return HTTP Response Assertion Builder */ public Builder body(String containsBody) { - this.bodyAssertion = BodyAssertion.builder().body(containsBody).contains(); - return this; + return body(BodyAssertion.builder().body(containsBody).contains()); } /** @@ -149,7 +145,10 @@ public Builder body(String containsBody) { * @return HTTP Response Assertion Builder */ public Builder body(BodyAssertion bodyAssertion) { - this.bodyAssertion = bodyAssertion; + if (this.bodyAssertions == null) { + this.bodyAssertions = new ArrayList<>(); + } + this.bodyAssertions.add(bodyAssertion); return this; } @@ -168,7 +167,7 @@ public Builder status(HttpStatus httpStatus) { * @return HTTP Response Assertion */ public HttpResponseAssertion build() { - return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, bodyAssertion, responseConsumer); + return new HttpResponseAssertion(Objects.requireNonNull(httpStatus), headers, bodyAssertions, responseConsumer); } } } From b057c7457fb34429649b0967b1994f3dcc1ec0f0 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 16:23:47 +0200 Subject: [PATCH 02/29] remove unused import --- .../server/tck/tests/ErrorNotFoundRouteExceptionHandlerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorNotFoundRouteExceptionHandlerTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorNotFoundRouteExceptionHandlerTest.java index 4f770a0b2f7..0b921f2f4c5 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorNotFoundRouteExceptionHandlerTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/ErrorNotFoundRouteExceptionHandlerTest.java @@ -21,7 +21,6 @@ import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Post; import io.micronaut.http.annotation.Produces; -import io.micronaut.http.server.exceptions.HttpStatusHandler; import io.micronaut.http.server.exceptions.NotAllowedException; import io.micronaut.http.server.exceptions.NotAllowedExceptionHandler; import io.micronaut.http.server.exceptions.response.ErrorResponseProcessor; From ad1e55bf9c527d5d3564b69538fd8e4b0c4e6828 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 17:33:50 +0200 Subject: [PATCH 03/29] remove svgs --- ...aultHtmlBodyErrorResponseProviderTest.java | 35 ++++++++ .../resources/i18n/messages_es.properties | 5 +- .../DefaultHtmlBodyErrorResponseProvider.java | 84 ++++--------------- .../HtmlBodyErrorResponseProviderTest.java | 9 -- 4 files changed, 56 insertions(+), 77 deletions(-) diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java index d23efd01738..3c576ba80a1 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java @@ -1,7 +1,10 @@ package io.micronaut.http.server.exceptions.response; +import io.micronaut.context.MessageSource; +import io.micronaut.context.annotation.Factory; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; +import io.micronaut.context.i18n.ResourceBundleMessageSource; import io.micronaut.core.annotation.Introspected; import io.micronaut.http.*; import io.micronaut.http.annotation.Body; @@ -15,6 +18,7 @@ import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; +import jakarta.inject.Singleton; import jakarta.validation.Valid; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @@ -52,6 +56,28 @@ void validationErrorsShowInHtmlErrorPages() { assertExpectedSubstringInHtml("", html); assertExpectedSubstringInHtml("book.author: must not be blank", html); assertExpectedSubstringInHtml("book.pages: must be less than or equal to 4032", html); + + + ex = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.GET("/paginanoencontrada").accept(MediaType.TEXT_HTML))); + assertEquals(HttpStatus.NOT_FOUND, ex.getStatus()); + htmlOptional = ex.getResponse().getBody(String.class); + assertTrue(htmlOptional.isPresent()); + html = htmlOptional.get(); + assertExpectedSubstringInHtml("", html); + assertExpectedSubstringInHtml("Not Found", html); + assertExpectedSubstringInHtml("The page you were looking for doesn’t exist", html); + assertExpectedSubstringInHtml("You may have mistyped the address or the page may have moved", html); + + + ex = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.GET("/paginanoencontrada").header(HttpHeaders.ACCEPT_LANGUAGE, "es").accept(MediaType.TEXT_HTML))); + assertEquals(HttpStatus.NOT_FOUND, ex.getStatus()); + htmlOptional = ex.getResponse().getBody(String.class); + assertTrue(htmlOptional.isPresent()); + html = htmlOptional.get(); + assertExpectedSubstringInHtml("", html); + assertExpectedSubstringInHtml("No encontrado", html); + assertExpectedSubstringInHtml("La página que buscabas no existe", html); + assertExpectedSubstringInHtml("Es posible que haya escrito mal la dirección o que la página se haya movido.", html); } private void assertExpectedSubstringInHtml(String expected, String html) { @@ -72,6 +98,15 @@ void save(@Body @Valid Book book) { } } + @Requires(property = "spec.name", value = "DefaultHtmlBodyErrorResponseProviderTest") + @Factory + static class MessageSourceFactory { + @Singleton + MessageSource createMessageSource() { + return new ResourceBundleMessageSource("i18n.messages"); + } + } + @Introspected record Book(@NotBlank String title, @NotBlank String author, @Max(4032 ) int pages) { } diff --git a/http-server-netty/src/test/resources/i18n/messages_es.properties b/http-server-netty/src/test/resources/i18n/messages_es.properties index e9a12f56573..0f5a8eadb85 100644 --- a/http-server-netty/src/test/resources/i18n/messages_es.properties +++ b/http-server-netty/src/test/resources/i18n/messages_es.properties @@ -1,2 +1,5 @@ hello=Hola -welcome.name=Bienvenido {0} \ No newline at end of file +welcome.name=Bienvenido {0} +404.error.bold=La p�gina que buscabas no existe +404.error.title=No encontrado +404.error=Es posible que haya escrito mal la direcci�n o que la p�gina se haya movido. \ No newline at end of file diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java index 21067c4e10d..6e4c97dfd9b 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java @@ -25,13 +25,11 @@ import jakarta.inject.Singleton; import java.text.MessageFormat; -import java.util.Collections; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static io.micronaut.http.HttpStatus.*; -import static io.micronaut.http.HttpStatus.CONNECTION_TIMED_OUT; /** * It generates HTML error response page for a given {@link HttpStatus}. @@ -43,71 +41,14 @@ final class DefaultHtmlBodyErrorResponseProvider implements HtmlBodyErrorResponseProvider { private static final Map DEFAULT_ERROR_BOLD = Map.of( NOT_FOUND, "The page you were looking for doesn’t exist", - REQUEST_ENTITY_TOO_LARGE, "The file or data you are trying to upload exceeds the allowed size", - BAD_GATEWAY, "The server received an invalid response", - SERVICE_UNAVAILABLE, "It may be overloaded, or undergoing scheduled maintenance. Please try again later", - GATEWAY_TIMEOUT, "The service did not respond within the allowed time limit"); + REQUEST_ENTITY_TOO_LARGE, "The file or data you are trying to upload exceeds the allowed size" + ); private static final Map DEFAULT_ERROR = Map.of( NOT_FOUND, "You may have mistyped the address or the page may have moved", - REQUEST_ENTITY_TOO_LARGE, "Please try again with a smaller file", - BAD_GATEWAY, "Please try again later", - SERVICE_UNAVAILABLE, "The service is temporarily unavailable", - GATEWAY_TIMEOUT, "It may be overloaded, or there may be a temporary technical issue. Please try again later" + REQUEST_ENTITY_TOO_LARGE, "Please try again with a smaller file" ); - private static final Map SVG; - - static { - Map svgs = new ConcurrentHashMap<>(); - svgs.put(BAD_REQUEST, ""); // 400 - svgs.put(UNAUTHORIZED, ""); // 401 - svgs.put(PAYMENT_REQUIRED, ""); // 402 - svgs.put(FORBIDDEN, ""); // 403 - svgs.put(NOT_FOUND, ""); // 404 - svgs.put(METHOD_NOT_ALLOWED, ""); // 405 - svgs.put(NOT_ACCEPTABLE, ""); // 406 - svgs.put(PROXY_AUTHENTICATION_REQUIRED, ""); // 407 - svgs.put(REQUEST_TIMEOUT, ""); // 408 - svgs.put(CONFLICT, ""); // 409 - svgs.put(GONE, ""); // 410 - svgs.put(LENGTH_REQUIRED, ""); // 411 - svgs.put(PRECONDITION_FAILED, ""); // 412 - svgs.put(REQUEST_ENTITY_TOO_LARGE, ""); // 413 - svgs.put(REQUEST_URI_TOO_LONG, ""); // 414 - svgs.put(UNSUPPORTED_MEDIA_TYPE, ""); // 415 - svgs.put(REQUESTED_RANGE_NOT_SATISFIABLE, ""); // 416 - svgs.put(EXPECTATION_FAILED, ""); // 417 - svgs.put(I_AM_A_TEAPOT, ""); // 418 - svgs.put(ENHANCE_YOUR_CALM, ""); // 420 - svgs.put(MISDIRECTED_REQUEST, ""); // 421 - svgs.put(UNPROCESSABLE_ENTITY, ""); // 422 - svgs.put(LOCKED, ""); // 423 - svgs.put(FAILED_DEPENDENCY, ""); // 424 - svgs.put(TOO_EARLY, ""); // 425 - svgs.put(UPGRADE_REQUIRED, ""); // 426 - svgs.put(PRECONDITION_REQUIRED, ""); // 428 - svgs.put(TOO_MANY_REQUESTS, ""); // 429 - svgs.put(REQUEST_HEADER_FIELDS_TOO_LARGE, ""); // 431 - svgs.put(NO_RESPONSE, ""); // 444 - svgs.put(BLOCKED_BY_WINDOWS_PARENTAL_CONTROLS, ""); // 450 - svgs.put(UNAVAILABLE_FOR_LEGAL_REASONS, ""); // 451 - svgs.put(REQUEST_HEADER_TOO_LARGE, ""); // 494 - svgs.put(INTERNAL_SERVER_ERROR, ""); // 500 - svgs.put(NOT_IMPLEMENTED, ""); // 501 - svgs.put(BAD_GATEWAY, ""); // 502 - svgs.put(SERVICE_UNAVAILABLE, ""); // 503 - svgs.put(GATEWAY_TIMEOUT, ""); // 504 - svgs.put(HTTP_VERSION_NOT_SUPPORTED, ""); // 505 - svgs.put(VARIANT_ALSO_NEGOTIATES, ""); // 506 - svgs.put(INSUFFICIENT_STORAGE, ""); // 507 - svgs.put(LOOP_DETECTED, ""); // 508 - svgs.put(BANDWIDTH_LIMIT_EXCEEDED, ""); // 509 - svgs.put(NOT_EXTENDED, ""); // 510 - svgs.put(NETWORK_AUTHENTICATION_REQUIRED, ""); // 511 - svgs.put(CONNECTION_TIMED_OUT, ""); // 522 - SVG = Collections.unmodifiableMap(svgs); - } private static final String CSS = """ *, *::before, *::after { box-sizing: border-box; @@ -118,6 +59,11 @@ final class DefaultHtmlBodyErrorResponseProvider implements HtmlBodyErrorRespons html { font-size: 16px; } + h2 { + margin-top: -0.95em; + font-size: 6em; + opacity: .2; + } body { background: #2559a7; color: #FFF; @@ -162,6 +108,7 @@ final class DefaultHtmlBodyErrorResponseProvider implements HtmlBodyErrorRespons width: 100%; } main article { + margin-top: -0.95em; width: min(100%, 30em); } main article p { @@ -193,18 +140,21 @@ public String body(@NonNull ErrorContext errorContext, @NonNull HttpResponse } private String html(Locale locale, HttpStatus status, ErrorContext errorContext) { - final String errorTitleCode = status + ".error.title"; + final String errorTitleCode = status.getCode() + ".error.title"; + final String errorTitle = messageSource.getMessage(errorTitleCode, status.getReason(), locale); + String header = "

" + errorTitle + "

"; + header += "

" + status.getCode() + "

"; return MessageFormat.format("{0} — {1}
{3}
{4}
", status.getCode(), - messageSource.getMessage(errorTitleCode, status.getReason(), locale), + errorTitle, CSS, - SVG.get(status), + header, article(locale, status, errorContext)); } private String article(Locale locale, HttpStatus status, ErrorContext errorContext) { - final String errorBoldCode = status + ".error.bold"; - final String errorCode = status + ".error"; + final String errorBoldCode = status.getCode() + ".error.bold"; + final String errorCode = status.getCode() + ".error"; String defaultErrorBold = DEFAULT_ERROR_BOLD.get(status); String defaultError = DEFAULT_ERROR.get(status); String errorBold = defaultErrorBold != null ? messageSource.getMessage(errorBoldCode, defaultErrorBold, locale) : messageSource.getMessage(errorBoldCode, locale).orElse(null); diff --git a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java index ef177bd603d..078dab571e4 100644 --- a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java +++ b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java @@ -87,15 +87,6 @@ public Optional getBody() { } else if (status.getCode() == 413) { assertExpectedSubstringInHtml("The file or data you are trying to upload exceeds the allowed size", html); assertExpectedSubstringInHtml("Please try again with a smaller file", html); - } else if (status.getCode() == 502) { - assertExpectedSubstringInHtml("The server received an invalid response", html); - assertExpectedSubstringInHtml("Please try again later", html); - } else if (status.getCode() == 503) { - assertExpectedSubstringInHtml("It may be overloaded, or undergoing scheduled maintenance. Please try again later", html); - assertExpectedSubstringInHtml("The service is temporarily unavailable", html); - } else if (status.getCode() == 504) { - assertExpectedSubstringInHtml("The service did not respond within the allowed time limit", html); - assertExpectedSubstringInHtml("It may be overloaded, or there may be a temporary technical issue. Please try again later", html); } } } From a36464e806064b6123641db2699fdede29b0d1e9 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 17:35:04 +0200 Subject: [PATCH 04/29] rename to error response body provider --- ...java => DefaultHtmlErrorResponseBodyProviderTest.java} | 6 +++--- .../response/DefaultErrorResponseProcessor.java | 8 ++++---- ...der.java => DefaultHtmlErrorResponseBodyProvider.java} | 6 +++--- ...der.java => DefaultJsonErrorResponseBodyProvider.java} | 6 +++--- ...sponseProvider.java => ErrorResponseBodyProvider.java} | 2 +- ...seProvider.java => HtmlErrorResponseBodyProvider.java} | 6 +++--- ...seProvider.java => JsonErrorResponseBodyProvider.java} | 4 ++-- ...erTest.java => HtmlErrorResponseBodyProviderTest.java} | 6 +++--- 8 files changed, 22 insertions(+), 22 deletions(-) rename http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/{DefaultHtmlBodyErrorResponseProviderTest.java => DefaultHtmlErrorResponseBodyProviderTest.java} (96%) rename http-server/src/main/java/io/micronaut/http/server/exceptions/response/{DefaultHtmlBodyErrorResponseProvider.java => DefaultHtmlErrorResponseBodyProvider.java} (97%) rename http-server/src/main/java/io/micronaut/http/server/exceptions/response/{DefaultJsonBodyErrorResponseProvider.java => DefaultJsonErrorResponseBodyProvider.java} (91%) rename http-server/src/main/java/io/micronaut/http/server/exceptions/response/{BodyErrorResponseProvider.java => ErrorResponseBodyProvider.java} (95%) rename http-server/src/main/java/io/micronaut/http/server/exceptions/response/{HtmlBodyErrorResponseProvider.java => HtmlErrorResponseBodyProvider.java} (82%) rename http-server/src/main/java/io/micronaut/http/server/exceptions/response/{JsonBodyErrorResponseProvider.java => JsonErrorResponseBodyProvider.java} (87%) rename http-server/src/test/java/io/micronaut/http/server/exceptions/response/{HtmlBodyErrorResponseProviderTest.java => HtmlErrorResponseBodyProviderTest.java} (95%) diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java similarity index 96% rename from http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java rename to http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java index 3c576ba80a1..d118cedf3ee 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProviderTest.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java @@ -35,11 +35,11 @@ @Property(name = "spec.name", value = "DefaultHtmlBodyErrorResponseProviderTest") @MicronautTest -class DefaultHtmlBodyErrorResponseProviderTest extends Specification { - private static final Logger LOG = LoggerFactory.getLogger(DefaultHtmlBodyErrorResponseProviderTest.class); +class DefaultHtmlErrorResponseBodyProviderTest extends Specification { + private static final Logger LOG = LoggerFactory.getLogger(DefaultHtmlErrorResponseBodyProviderTest.class); @Inject - HtmlBodyErrorResponseProvider htmlProvider; + HtmlErrorResponseBodyProvider htmlProvider; @Client("/") @Inject diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java index 3fe1f52f68b..039aad095e7 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -25,7 +25,7 @@ /** * Default implementation of {@link ErrorResponseProcessor}. - * It delegates to {@link JsonBodyErrorResponseProvider} for JSON responses and to {@link HtmlBodyErrorResponseProvider} for HTML responses. + * It delegates to {@link JsonErrorResponseBodyProvider} for JSON responses and to {@link HtmlErrorResponseBodyProvider} for HTML responses. * * @author Sergio del Amo * @since 4.7.0 @@ -34,10 +34,10 @@ @Singleton @Secondary final class DefaultErrorResponseProcessor implements ErrorResponseProcessor { - private final JsonBodyErrorResponseProvider jsonBodyErrorResponseProvider; - private final HtmlBodyErrorResponseProvider htmlBodyErrorResponseProvider; + private final JsonErrorResponseBodyProvider jsonBodyErrorResponseProvider; + private final HtmlErrorResponseBodyProvider htmlBodyErrorResponseProvider; - DefaultErrorResponseProcessor(JsonBodyErrorResponseProvider jsonBodyErrorResponseProvider, HtmlBodyErrorResponseProvider htmlBodyErrorResponseProvider) { + DefaultErrorResponseProcessor(JsonErrorResponseBodyProvider jsonBodyErrorResponseProvider, HtmlErrorResponseBodyProvider htmlBodyErrorResponseProvider) { this.jsonBodyErrorResponseProvider = jsonBodyErrorResponseProvider; this.htmlBodyErrorResponseProvider = htmlBodyErrorResponseProvider; } diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java similarity index 97% rename from http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java rename to http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java index 6e4c97dfd9b..864e2600b59 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlBodyErrorResponseProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java @@ -38,7 +38,7 @@ */ @Internal @Singleton -final class DefaultHtmlBodyErrorResponseProvider implements HtmlBodyErrorResponseProvider { +final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBodyProvider { private static final Map DEFAULT_ERROR_BOLD = Map.of( NOT_FOUND, "The page you were looking for doesn’t exist", REQUEST_ENTITY_TOO_LARGE, "The file or data you are trying to upload exceeds the allowed size" @@ -126,8 +126,8 @@ final class DefaultHtmlBodyErrorResponseProvider implements HtmlBodyErrorRespons private final LocaleResolver> localeResolver; private final Map cache = new ConcurrentHashMap<>(); - DefaultHtmlBodyErrorResponseProvider(MessageSource messageSource, - LocaleResolver> localeResolver) { + DefaultHtmlErrorResponseBodyProvider(MessageSource messageSource, + LocaleResolver> localeResolver) { this.messageSource = messageSource; this.localeResolver = localeResolver; } diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java similarity index 91% rename from http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonBodyErrorResponseProvider.java rename to http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java index 5760dd2371d..07608a51a33 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonBodyErrorResponseProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java @@ -27,16 +27,16 @@ import java.util.List; /** - * Default implementation of {@link JsonBodyErrorResponseProvider} which returns a {@link JsonError}. + * Default implementation of {@link JsonErrorResponseBodyProvider} which returns a {@link JsonError}. * * @since 4.7.0 */ @Internal @Singleton -class DefaultJsonBodyErrorResponseProvider implements JsonBodyErrorResponseProvider { +class DefaultJsonErrorResponseBodyProvider implements JsonErrorResponseBodyProvider { private final boolean alwaysSerializeErrorsAsList; - DefaultJsonBodyErrorResponseProvider(JsonConfiguration jacksonConfiguration) { + DefaultJsonErrorResponseBodyProvider(JsonConfiguration jacksonConfiguration) { this.alwaysSerializeErrorsAsList = jacksonConfiguration.isAlwaysSerializeErrorsAsList(); } diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/BodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java similarity index 95% rename from http-server/src/main/java/io/micronaut/http/server/exceptions/response/BodyErrorResponseProvider.java rename to http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java index ad7f7681390..4a62c65f461 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/BodyErrorResponseProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java @@ -24,7 +24,7 @@ * @since 4.7.0 * @param The body type */ -public interface BodyErrorResponseProvider { +public interface ErrorResponseBodyProvider { @NonNull T body(@NonNull ErrorContext errorContext, @NonNull HttpResponse response); diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProvider.java similarity index 82% rename from http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProvider.java rename to http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProvider.java index 4246c1781a4..c0f7b600fb5 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProvider.java @@ -19,14 +19,14 @@ import io.micronaut.http.MediaType; /** - * A {@link BodyErrorResponseProvider} for HTML responses. + * A {@link ErrorResponseBodyProvider} for HTML responses. * Responses with content type {@link io.micronaut.http.MediaType#TEXT_HTML}. * @author Sergio del Amo * @since 4.7.0 * @param The body type */ -@DefaultImplementation(DefaultHtmlBodyErrorResponseProvider.class) -public interface HtmlBodyErrorResponseProvider extends BodyErrorResponseProvider { +@DefaultImplementation(DefaultHtmlErrorResponseBodyProvider.class) +public interface HtmlErrorResponseBodyProvider extends ErrorResponseBodyProvider { @Override default String contentType() { diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonBodyErrorResponseProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonErrorResponseBodyProvider.java similarity index 87% rename from http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonBodyErrorResponseProvider.java rename to http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonErrorResponseBodyProvider.java index 4a72f663876..53660e31354 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonBodyErrorResponseProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonErrorResponseBodyProvider.java @@ -19,14 +19,14 @@ import jakarta.inject.Singleton; /** - * A {@link BodyErrorResponseProvider} for JSON responses. + * A {@link ErrorResponseBodyProvider} for JSON responses. * Responses with content type {@link io.micronaut.http.MediaType#APPLICATION_JSON}. * @author Sergio del Amo * @since 4.7.0 * @param The error type */ @Singleton -public interface JsonBodyErrorResponseProvider extends BodyErrorResponseProvider { +public interface JsonErrorResponseBodyProvider extends ErrorResponseBodyProvider { @Override default String contentType() { return MediaType.APPLICATION_JSON; diff --git a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java similarity index 95% rename from http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java rename to http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java index 078dab571e4..ad04a0b8d35 100644 --- a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlBodyErrorResponseProviderTest.java +++ b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java @@ -18,11 +18,11 @@ import static org.junit.jupiter.api.Assertions.*; @MicronautTest(startApplication = false) -class HtmlBodyErrorResponseProviderTest extends Specification { - private static final Logger LOG = LoggerFactory.getLogger(HtmlBodyErrorResponseProviderTest.class); +class HtmlErrorResponseBodyProviderTest extends Specification { + private static final Logger LOG = LoggerFactory.getLogger(HtmlErrorResponseBodyProviderTest.class); @Inject - HtmlBodyErrorResponseProvider htmlProvider; + HtmlErrorResponseBodyProvider htmlProvider; @ParameterizedTest @EnumSource(HttpStatus.class) From 845e94ab174d24e55800c1af9876e58719e8e2d6 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 17:36:09 +0200 Subject: [PATCH 05/29] extract request into a local variable and reuse --- .../exceptions/response/DefaultErrorResponseProcessor.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java index 039aad095e7..1e8d51401a6 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -18,6 +18,7 @@ import io.micronaut.context.annotation.Secondary; import io.micronaut.core.annotation.Internal; import io.micronaut.http.HttpMethod; +import io.micronaut.http.HttpRequest; import io.micronaut.http.MediaType; import io.micronaut.http.MutableHttpResponse; import io.micronaut.http.hateoas.JsonError; @@ -44,11 +45,12 @@ final class DefaultErrorResponseProcessor implements ErrorResponseProcessor { @Override public MutableHttpResponse processResponse(ErrorContext errorContext, MutableHttpResponse response) { - if (errorContext.getRequest().getMethod() == HttpMethod.HEAD) { + HttpRequest request = errorContext.getRequest(); + if (request.getMethod() == HttpMethod.HEAD) { return (MutableHttpResponse) response; } final boolean isError = response.status().getCode() >= 400; - if (errorContext.getRequest().accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE)) && isError) { + if (request.accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE)) && isError) { return response.body(htmlBodyErrorResponseProvider.body(errorContext, response)) .contentType(htmlBodyErrorResponseProvider.contentType()); } From 5fd2b03688f47d57440c13c4d1e8b11dbc5b48cc Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 17:36:43 +0200 Subject: [PATCH 06/29] flip the condition --- .../exceptions/response/DefaultErrorResponseProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java index 1e8d51401a6..f38920b21ce 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -50,7 +50,7 @@ public MutableHttpResponse processResponse(ErrorContext errorContext, MutableHtt return (MutableHttpResponse) response; } final boolean isError = response.status().getCode() >= 400; - if (request.accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE)) && isError) { + if (isError && request.accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE))) { return response.body(htmlBodyErrorResponseProvider.body(errorContext, response)) .contentType(htmlBodyErrorResponseProvider.contentType()); } From 4ded5ae25b2f9e0aba7744bd7505211e5ba6883e Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 17:37:07 +0200 Subject: [PATCH 07/29] make class final --- .../response/DefaultJsonErrorResponseBodyProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java index 07608a51a33..8fd53b5acb1 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java @@ -33,7 +33,7 @@ */ @Internal @Singleton -class DefaultJsonErrorResponseBodyProvider implements JsonErrorResponseBodyProvider { +final class DefaultJsonErrorResponseBodyProvider implements JsonErrorResponseBodyProvider { private final boolean alwaysSerializeErrorsAsList; DefaultJsonErrorResponseBodyProvider(JsonConfiguration jacksonConfiguration) { From 0136f34b1ac065aa73ca2e890d7787a602661e7b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 17:40:19 +0200 Subject: [PATCH 08/29] use @Requires missing beans instead of Secondary --- .../exceptions/response/DefaultErrorResponseProcessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java index f38920b21ce..c0de16636c3 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -15,7 +15,7 @@ */ package io.micronaut.http.server.exceptions.response; -import io.micronaut.context.annotation.Secondary; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.http.HttpMethod; import io.micronaut.http.HttpRequest; @@ -33,7 +33,7 @@ */ @Internal @Singleton -@Secondary +@Requires(missingBeans = ErrorResponseProcessor.class) final class DefaultErrorResponseProcessor implements ErrorResponseProcessor { private final JsonErrorResponseBodyProvider jsonBodyErrorResponseProvider; private final HtmlErrorResponseBodyProvider htmlBodyErrorResponseProvider; From a50ca6d37c75ea906ee4d220056acb649853bbda Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Fri, 27 Sep 2024 20:47:16 +0200 Subject: [PATCH 09/29] fill empty method --- .../response/DefaultHtmlErrorResponseBodyProviderTest.java | 1 + .../http/server/tck/tests/exceptions/HtmlErrorPageTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java index d118cedf3ee..374e889498d 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java @@ -95,6 +95,7 @@ static class FooController { @Post("/save") @Status(HttpStatus.CREATED) void save(@Body @Valid Book book) { + throw new UnsupportedOperationException(); } } diff --git a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java index d17700d7bea..49ed714e284 100644 --- a/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java +++ b/http-server-tck/src/main/java/io/micronaut/http/server/tck/tests/exceptions/HtmlErrorPageTest.java @@ -69,6 +69,7 @@ static class FooController { @Post("/save") @Status(HttpStatus.CREATED) void save(@Body @Valid Book book) { + throw new UnsupportedOperationException(); } } From 6f5b14c9be42ab2581f683b717942492c5d08968 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 12:10:29 +0200 Subject: [PATCH 10/29] Update http-server-netty/src/test/resources/logback.xml Co-authored-by: Jonas Konrad --- http-server-netty/src/test/resources/logback.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/http-server-netty/src/test/resources/logback.xml b/http-server-netty/src/test/resources/logback.xml index d32bf51310a..e315d7867e6 100644 --- a/http-server-netty/src/test/resources/logback.xml +++ b/http-server-netty/src/test/resources/logback.xml @@ -24,5 +24,4 @@ - From a288899b7f56f4fcc3e59349dfa19c05d79a23ca Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 12:11:23 +0200 Subject: [PATCH 11/29] Update http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java Co-authored-by: Jonas Konrad --- .../server/exceptions/response/ErrorResponseBodyProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java index 4a62c65f461..446d70e7b52 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java @@ -22,7 +22,7 @@ * Provides an HTTP Response body of an error response. * @author Sergio del Amo * @since 4.7.0 - * @param The body type + * @param The body type */ public interface ErrorResponseBodyProvider { @NonNull From de6f89cbe94e20742b0a61f4938de6c1124e5bbb Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 12:14:50 +0200 Subject: [PATCH 12/29] initialize arraylist with size --- .../response/DefaultJsonErrorResponseBodyProvider.java | 2 +- .../exceptions/response/HateoasErrorResponseProcessor.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java index 8fd53b5acb1..e6981568db4 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java @@ -51,7 +51,7 @@ public JsonError body(ErrorContext errorContext, HttpResponse response) { jsonError.getPath().ifPresent(error::path); } else { error = new JsonError(response.reason()); - List errors = new ArrayList<>(); + List errors = new ArrayList<>(errorContext.getErrors().size()); for (Error jsonError : errorContext.getErrors()) { errors.add(new JsonError(jsonError.getMessage()).path(jsonError.getPath().orElse(null))); } diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java index 4e0ac3579e7..0d3e12154ef 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HateoasErrorResponseProcessor.java @@ -57,7 +57,7 @@ public MutableHttpResponse processResponse(@NonNull ErrorContext erro jsonError.getPath().ifPresent(error::path); } else { error = new JsonError(response.reason()); - List errors = new ArrayList<>(); + List errors = new ArrayList<>(errorContext.getErrors().size()); for (Error jsonError : errorContext.getErrors()) { errors.add(new JsonError(jsonError.getMessage()).path(jsonError.getPath().orElse(null))); } From 031a604869e88b3b6ee021a053370385e38ab1d6 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 12:16:08 +0200 Subject: [PATCH 13/29] javadoc: add missing javadoc --- .../exceptions/response/ErrorResponseBodyProvider.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java index 446d70e7b52..6c48e454def 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/ErrorResponseBodyProvider.java @@ -25,9 +25,18 @@ * @param The body type */ public interface ErrorResponseBodyProvider { + /** + * + * @param errorContext Error Context + * @param response Base HTTP Response + * @return The HTTP Response Body + */ @NonNull T body(@NonNull ErrorContext errorContext, @NonNull HttpResponse response); + /** + * @return The content type of the HTTP response + */ @NonNull String contentType(); From 810bc55238d6f26381b0e4a88cb1bee8a3402215 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 12:46:47 +0200 Subject: [PATCH 14/29] check request does not accept json --- ...aultHtmlErrorResponseBodyProviderTest.java | 22 ++++- .../DefaultErrorResponseProcessor.java | 10 +-- http/build.gradle | 1 + .../io/micronaut/http/MediaTypeUtils.java | 44 ++++++++++ .../io/micronaut/http/MediaTypeUtilsTest.java | 85 +++++++++++++++++++ 5 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 http/src/main/java/io/micronaut/http/MediaTypeUtils.java create mode 100644 http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java index 374e889498d..90b6fa6003f 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java @@ -29,6 +29,7 @@ import java.util.Optional; +import static org.junit.Assert.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -45,11 +46,30 @@ class DefaultHtmlErrorResponseBodyProviderTest extends Specification { @Inject HttpClient httpClient; + @Test + void ifRequestAcceptsBothJsonAnHtmlJsonIsUsed() { + BlockingHttpClient client = httpClient.toBlocking(); + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> + client.exchange(HttpRequest.POST("/book/save", new Book("Building Microservices", "", 5000)) + .accept(MediaType.TEXT_HTML, MediaType.APPLICATION_JSON))); + assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus()); + assertTrue(ex.getResponse().getContentType().isPresent()); + assertEquals(MediaType.APPLICATION_JSON, ex.getResponse().getContentType().get().toString()); + Optional jsonOptional = ex.getResponse().getBody(String.class); + assertTrue(jsonOptional.isPresent()); + String json = jsonOptional.get(); + assertFalse(json.contains("")); + } + @Test void validationErrorsShowInHtmlErrorPages() { BlockingHttpClient client = httpClient.toBlocking(); - HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> client.exchange(HttpRequest.POST("/book/save", new Book("Building Microservices", "", 5000)).accept(MediaType.TEXT_HTML))); + HttpClientResponseException ex = assertThrows(HttpClientResponseException.class, () -> + client.exchange(HttpRequest.POST("/book/save", new Book("Building Microservices", "", 5000)) + .accept(MediaType.TEXT_HTML))); assertEquals(HttpStatus.BAD_REQUEST, ex.getStatus()); + assertTrue(ex.getResponse().getContentType().isPresent()); + assertEquals(MediaType.TEXT_HTML, ex.getResponse().getContentType().get().toString()); Optional htmlOptional = ex.getResponse().getBody(String.class); assertTrue(htmlOptional.isPresent()); String html = htmlOptional.get(); diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java index c0de16636c3..12fdbdae091 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -17,10 +17,7 @@ import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; -import io.micronaut.http.HttpMethod; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.MediaType; -import io.micronaut.http.MutableHttpResponse; +import io.micronaut.http.*; import io.micronaut.http.hateoas.JsonError; import jakarta.inject.Singleton; @@ -50,7 +47,10 @@ public MutableHttpResponse processResponse(ErrorContext errorContext, MutableHtt return (MutableHttpResponse) response; } final boolean isError = response.status().getCode() >= 400; - if (isError && request.accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE))) { + if (isError + && request.accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE)) + && request.accept().stream().noneMatch(MediaTypeUtils::isJson) + ) { return response.body(htmlBodyErrorResponseProvider.body(errorContext, response)) .contentType(htmlBodyErrorResponseProvider.contentType()); } diff --git a/http/build.gradle b/http/build.gradle index 9c40ac1d5e9..fb64334a79e 100644 --- a/http/build.gradle +++ b/http/build.gradle @@ -22,6 +22,7 @@ dependencies { testImplementation project(":runtime") testImplementation(libs.logback.classic) testImplementation(libs.jazzer.junit) + testImplementation(libs.junit.jupiter.params) } tasks.named("compileKotlin") { diff --git a/http/src/main/java/io/micronaut/http/MediaTypeUtils.java b/http/src/main/java/io/micronaut/http/MediaTypeUtils.java new file mode 100644 index 00000000000..45a281626c9 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/MediaTypeUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http; + +import io.micronaut.core.annotation.NonNull; + +/** + * Utility methods for working with {@link MediaType}. + * @author Sergio del Amo + * @since 4.7.0 + */ +public final class MediaTypeUtils { + + /** + * + * @param mediaType Media Type + * @return Returns true if the media type is {@link MediaType#APPLICATION_JSON_TYPE}, {@link MediaType#TEXT_JSON_TYPE}, {@link MediaType#APPLICATION_HAL_JSON_TYPE}, {@link MediaType#APPLICATION_JSON_GITHUB_TYPE}, {@link MediaType#APPLICATION_JSON_FEED_TYPE}, {@link {@link MediaType#APPLICATION_JSON_PROBLEM_TYPE}, {@link MediaType#APPLICATION_JSON_PATCH_TYPE}, {@link MediaType#APPLICATION_JSON_MERGE_PATCH_TYPE} or {@link MediaType#APPLICATION_JSON_SCHEMA_TYPE}. + */ + public static boolean isJson(@NonNull MediaType mediaType) { + return mediaType.equals(MediaType.APPLICATION_JSON_TYPE) + || mediaType.equals(MediaType.TEXT_JSON_TYPE) + || mediaType.equals(MediaType.APPLICATION_HAL_JSON_TYPE) + || mediaType.equals(MediaType.APPLICATION_JSON_GITHUB_TYPE) + || mediaType.equals(MediaType.APPLICATION_JSON_FEED_TYPE) + || mediaType.equals(MediaType.APPLICATION_JSON_PROBLEM_TYPE) + || mediaType.equals(MediaType.APPLICATION_JSON_PATCH_TYPE) + || mediaType.equals(MediaType.APPLICATION_JSON_MERGE_PATCH_TYPE) + || mediaType.equals(MediaType.APPLICATION_JSON_SCHEMA_TYPE); + + } +} diff --git a/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java b/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java new file mode 100644 index 00000000000..adbb29083be --- /dev/null +++ b/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java @@ -0,0 +1,85 @@ +package io.micronaut.http; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; + +import static io.micronaut.http.MediaType.*; +import static org.junit.jupiter.api.Assertions.*; + +class MediaTypeUtilsTest { + @ParameterizedTest + @MethodSource + void isJsonTrue(MediaType mediaType) { + assertTrue(MediaTypeUtils.isJson(mediaType)); + } + + private static List isJsonTrue() { + return List.of( + APPLICATION_JSON_TYPE, + TEXT_JSON_TYPE, + APPLICATION_HAL_JSON_TYPE, + APPLICATION_JSON_GITHUB_TYPE, + APPLICATION_JSON_FEED_TYPE, + APPLICATION_JSON_PROBLEM_TYPE, + APPLICATION_JSON_PATCH_TYPE, + APPLICATION_JSON_MERGE_PATCH_TYPE, + APPLICATION_JSON_SCHEMA_TYPE + ); + } + + @ParameterizedTest + @MethodSource + void isJsonFalse(MediaType mediaType) { + assertFalse(MediaTypeUtils.isJson(mediaType)); + } + + private static List isJsonFalse() { + return List.of(ALL_TYPE, + APPLICATION_FORM_URLENCODED_TYPE, + APPLICATION_XHTML_TYPE, + APPLICATION_XML_TYPE, + APPLICATION_YAML_TYPE, + APPLICATION_HAL_XML_TYPE, + APPLICATION_ATOM_XML_TYPE, + APPLICATION_VND_ERROR_TYPE, + APPLICATION_JSON_STREAM_TYPE, + APPLICATION_OCTET_STREAM_TYPE, + APPLICATION_GRAPHQL_TYPE, + APPLICATION_PDF_TYPE, + GPX_XML_TYPE, + GZIP_TYPE, + ZIP_TYPE, + MICROSOFT_EXCEL_OPEN_XML_TYPE, + MICROSOFT_EXCEL_TYPE, + YANG_TYPE, + CUE_TYPE, + TOML_TYPE, + RTF_TYPE, + ZLIB_TYPE, + ZSTD_TYPE, + MULTIPART_FORM_DATA_TYPE, + TEXT_HTML_TYPE, + TEXT_CSV_TYPE, + TEXT_XML_TYPE, + TEXT_PLAIN_TYPE, + TEXT_EVENT_STREAM_TYPE, + TEXT_MARKDOWN_TYPE, + TEXT_CSS_TYPE, + TEXT_JAVASCRIPT_TYPE, + TEXT_ECMASCRIPT_TYPE, + IMAGE_APNG_TYPE, + IMAGE_BMP_TYPE, + IMAGE_X_ICON_TYPE, + IMAGE_TIFF_TYPE, + IMAGE_AVIF_TYPE, + IMAGE_SVG_TYPE, + IMAGE_XBM_TYPE, + IMAGE_PNG_TYPE, + IMAGE_JPEG_TYPE, + IMAGE_GIF_TYPE, + IMAGE_WEBP_TYPE, + IMAGE_WMF_TYPE); + } +} From 6918fc35ca248f1b0e39f46649e8477c07f91553 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 12:50:06 +0200 Subject: [PATCH 15/29] for HtmlErrorResponseBodyProvider specify generic as String --- .../DefaultErrorResponseProcessor.java | 5 ++-- .../DefaultHtmlErrorResponseBodyProvider.java | 28 +++++++++---------- .../HtmlErrorResponseBodyProvider.java | 3 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java index 12fdbdae091..90f149a5cb2 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -33,9 +33,10 @@ @Requires(missingBeans = ErrorResponseProcessor.class) final class DefaultErrorResponseProcessor implements ErrorResponseProcessor { private final JsonErrorResponseBodyProvider jsonBodyErrorResponseProvider; - private final HtmlErrorResponseBodyProvider htmlBodyErrorResponseProvider; + private final HtmlErrorResponseBodyProvider htmlBodyErrorResponseProvider; - DefaultErrorResponseProcessor(JsonErrorResponseBodyProvider jsonBodyErrorResponseProvider, HtmlErrorResponseBodyProvider htmlBodyErrorResponseProvider) { + DefaultErrorResponseProcessor(JsonErrorResponseBodyProvider jsonBodyErrorResponseProvider, + HtmlErrorResponseBodyProvider htmlBodyErrorResponseProvider) { this.jsonBodyErrorResponseProvider = jsonBodyErrorResponseProvider; this.htmlBodyErrorResponseProvider = htmlBodyErrorResponseProvider; } diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java index 864e2600b59..67042ff3bc3 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java @@ -38,7 +38,7 @@ */ @Internal @Singleton -final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBodyProvider { +final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBodyProvider { private static final Map DEFAULT_ERROR_BOLD = Map.of( NOT_FOUND, "The page you were looking for doesn’t exist", REQUEST_ENTITY_TOO_LARGE, "The file or data you are trying to upload exceeds the allowed size" @@ -49,7 +49,7 @@ final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBod REQUEST_ENTITY_TOO_LARGE, "Please try again with a smaller file" ); - private static final String CSS = """ + private static final String CSS = """ *, *::before, *::after { box-sizing: border-box; } @@ -85,41 +85,41 @@ final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBod font-weight: 700; text-decoration: underline; text-underline-offset: 0.0925em; - } + } b, strong { font-weight: 700; - } + } i, em { font-style: italic; - } + } main { display: grid; gap: 1em; padding: 2em; place-items: center; text-align: center; - } + } main header { width: min(100%, 18em); - } + } main header svg { height: auto; max-width: 100%; width: 100%; - } + } main article { margin-top: -0.95em; width: min(100%, 30em); - } + } main article p { font-size: 75%; - } - main article br { - display: none; + } + main article br { + display: none; @media(min-width: 48em) { display: inline; - } - } + } + } """; private final MessageSource messageSource; diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProvider.java index c0f7b600fb5..a45b142f74e 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProvider.java @@ -23,10 +23,9 @@ * Responses with content type {@link io.micronaut.http.MediaType#TEXT_HTML}. * @author Sergio del Amo * @since 4.7.0 - * @param The body type */ @DefaultImplementation(DefaultHtmlErrorResponseBodyProvider.class) -public interface HtmlErrorResponseBodyProvider extends ErrorResponseBodyProvider { +public interface HtmlErrorResponseBodyProvider extends ErrorResponseBodyProvider { @Override default String contentType() { From 810eb8ed5390ab1f4ac4b3437825cb2c8e627d02 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 14:14:41 +0200 Subject: [PATCH 16/29] =?UTF-8?q?don=E2=80=99t=20use=20generic=20for=20Htm?= =?UTF-8?q?lErrorResponseBodyProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/DefaultHtmlErrorResponseBodyProviderTest.java | 2 +- .../exceptions/response/HtmlErrorResponseBodyProviderTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java index 90b6fa6003f..12d15963d4d 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java @@ -40,7 +40,7 @@ class DefaultHtmlErrorResponseBodyProviderTest extends Specification { private static final Logger LOG = LoggerFactory.getLogger(DefaultHtmlErrorResponseBodyProviderTest.class); @Inject - HtmlErrorResponseBodyProvider htmlProvider; + HtmlErrorResponseBodyProvider htmlProvider; @Client("/") @Inject diff --git a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java index ad04a0b8d35..3096dc1b4f2 100644 --- a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java +++ b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java @@ -22,7 +22,7 @@ class HtmlErrorResponseBodyProviderTest extends Specification { private static final Logger LOG = LoggerFactory.getLogger(HtmlErrorResponseBodyProviderTest.class); @Inject - HtmlErrorResponseBodyProvider htmlProvider; + HtmlErrorResponseBodyProvider htmlProvider; @ParameterizedTest @EnumSource(HttpStatus.class) From 4b7c7d16ca609754bfe7caaab5588a2ba67da4a5 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 14:14:49 +0200 Subject: [PATCH 17/29] test matchesExtension --- http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java b/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java index adbb29083be..32ad96bc481 100644 --- a/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java +++ b/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java @@ -13,6 +13,7 @@ class MediaTypeUtilsTest { @MethodSource void isJsonTrue(MediaType mediaType) { assertTrue(MediaTypeUtils.isJson(mediaType)); + assertTrue(mediaType.matchesExtension(MediaType.EXTENSION_JSON)); } private static List isJsonTrue() { From 27a15c35d0938d25fd9fd65231b823cab430839c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Mon, 30 Sep 2024 14:27:33 +0200 Subject: [PATCH 18/29] Use HttpResponse:code() and HttpResponse::reason() --- .../DefaultHtmlErrorResponseBodyProvider.java | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java index 67042ff3bc3..2c8ece14419 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java @@ -39,14 +39,14 @@ @Internal @Singleton final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBodyProvider { - private static final Map DEFAULT_ERROR_BOLD = Map.of( - NOT_FOUND, "The page you were looking for doesn’t exist", - REQUEST_ENTITY_TOO_LARGE, "The file or data you are trying to upload exceeds the allowed size" + private static final Map DEFAULT_ERROR_BOLD = Map.of( + NOT_FOUND.getCode(), "The page you were looking for doesn’t exist", + REQUEST_ENTITY_TOO_LARGE.getCode(), "The file or data you are trying to upload exceeds the allowed size" ); - private static final Map DEFAULT_ERROR = Map.of( - NOT_FOUND, "You may have mistyped the address or the page may have moved", - REQUEST_ENTITY_TOO_LARGE, "Please try again with a smaller file" + private static final Map DEFAULT_ERROR = Map.of( + NOT_FOUND.getCode(), "You may have mistyped the address or the page may have moved", + REQUEST_ENTITY_TOO_LARGE.getCode(), "Please try again with a smaller file" ); private static final String CSS = """ @@ -135,34 +135,42 @@ final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBod @Override public String body(@NonNull ErrorContext errorContext, @NonNull HttpResponse response) { final HttpStatus status = response.status(); + int httpStatusCode = response.code(); + String httpStatusReason = response.reason(); Locale locale = localeResolver.resolveOrDefault(errorContext.getRequest()); - return cache.computeIfAbsent(new LocaleStatus(locale, status), key -> html(locale, status, errorContext)); + return cache.computeIfAbsent(new LocaleStatus(locale, httpStatusCode), key -> html(locale, httpStatusCode, httpStatusReason, errorContext)); } - private String html(Locale locale, HttpStatus status, ErrorContext errorContext) { - final String errorTitleCode = status.getCode() + ".error.title"; - final String errorTitle = messageSource.getMessage(errorTitleCode, status.getReason(), locale); + private String html(Locale locale, + int httpStatusCode, + String httpStatusReason, + ErrorContext errorContext) { + final String errorTitleCode = httpStatusCode + ".error.title"; + final String errorTitle = messageSource.getMessage(errorTitleCode, httpStatusReason, locale); String header = "

" + errorTitle + "

"; - header += "

" + status.getCode() + "

"; + header += "

" + httpStatusCode + "

"; return MessageFormat.format("{0} — {1}
{3}
{4}
", - status.getCode(), + httpStatusCode, errorTitle, CSS, header, - article(locale, status, errorContext)); + article(locale, httpStatusCode, httpStatusReason, errorContext)); } - private String article(Locale locale, HttpStatus status, ErrorContext errorContext) { - final String errorBoldCode = status.getCode() + ".error.bold"; - final String errorCode = status.getCode() + ".error"; - String defaultErrorBold = DEFAULT_ERROR_BOLD.get(status); - String defaultError = DEFAULT_ERROR.get(status); + private String article(Locale locale, + int httpStatusCode, + String httpStatusReason, + ErrorContext errorContext) { + final String errorBoldCode = httpStatusCode + ".error.bold"; + final String errorCode = httpStatusCode + ".error"; + String defaultErrorBold = DEFAULT_ERROR_BOLD.get(httpStatusCode); + String defaultError = DEFAULT_ERROR.get(httpStatusCode); String errorBold = defaultErrorBold != null ? messageSource.getMessage(errorBoldCode, defaultErrorBold, locale) : messageSource.getMessage(errorBoldCode, locale).orElse(null); String error = defaultError != null ? messageSource.getMessage(errorCode, defaultError, locale) : messageSource.getMessage(errorCode, locale).orElse(null); StringBuilder sb = new StringBuilder(); for (io.micronaut.http.server.exceptions.response.Error e : errorContext.getErrors()) { - if (!e.getMessage().equalsIgnoreCase(status.getReason())) { + if (!e.getMessage().equalsIgnoreCase(httpStatusReason)) { sb.append(e.getMessage()); sb.append("
"); } @@ -184,7 +192,7 @@ private String article(Locale locale, HttpStatus status, ErrorContext errorConte return sb.toString(); } - private record LocaleStatus(Locale locale, HttpStatus status) { + private record LocaleStatus(Locale locale, int httpStatusCode) { } } From 6d63118a568bd75f7485640abaca083fafa7a81f Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 12:34:58 +0200 Subject: [PATCH 19/29] use MediaType::matchesExtensions --- .../DefaultErrorResponseProcessor.java | 2 +- .../java/io/micronaut/http/MediaType.java | 12 ++++- .../io/micronaut/http/MediaTypeUtils.java | 44 ------------------- ...aTypeUtilsTest.java => MediaTypeTest.java} | 6 +-- .../json/body/JsonMessageHandler.java | 4 +- 5 files changed, 17 insertions(+), 51 deletions(-) delete mode 100644 http/src/main/java/io/micronaut/http/MediaTypeUtils.java rename http/src/test/java/io/micronaut/http/{MediaTypeUtilsTest.java => MediaTypeTest.java} (92%) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java index 90f149a5cb2..b31a913650f 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultErrorResponseProcessor.java @@ -50,7 +50,7 @@ public MutableHttpResponse processResponse(ErrorContext errorContext, MutableHtt final boolean isError = response.status().getCode() >= 400; if (isError && request.accept().stream().anyMatch(mediaType -> mediaType.equals(MediaType.TEXT_HTML_TYPE)) - && request.accept().stream().noneMatch(MediaTypeUtils::isJson) + && request.accept().stream().noneMatch(m -> m.matchesExtension(MediaType.EXTENSION_JSON)) ) { return response.body(htmlBodyErrorResponseProvider.body(errorContext, response)) .contentType(htmlBodyErrorResponseProvider.contentType()); diff --git a/http/src/main/java/io/micronaut/http/MediaType.java b/http/src/main/java/io/micronaut/http/MediaType.java index a6f398d5b13..db86b174f37 100644 --- a/http/src/main/java/io/micronaut/http/MediaType.java +++ b/http/src/main/java/io/micronaut/http/MediaType.java @@ -910,6 +910,16 @@ public boolean matchesType(String matchType) { return type.equals(WILDCARD) || type.equalsIgnoreCase(matchType); } + /** + * Check if the extension matches. + * @param matchExtension The extension to match + * @return true if matches + * @since 4.7.0 + */ + public boolean matchesAllOrWildcardOrExtension(String matchExtension) { + return extension.equalsIgnoreCase(ALL_TYPE.extension) || extension.equals(WILDCARD) || matchesExtension(matchExtension); + } + /** * Check if the extension matches. * @param matchExtension The extension to match @@ -917,7 +927,7 @@ public boolean matchesType(String matchType) { * @since 4.6.3 */ public boolean matchesExtension(String matchExtension) { - return extension.equalsIgnoreCase(ALL_TYPE.extension) || extension.equals(WILDCARD) || extension.equalsIgnoreCase(matchExtension); + return extension.equalsIgnoreCase(matchExtension); } /** diff --git a/http/src/main/java/io/micronaut/http/MediaTypeUtils.java b/http/src/main/java/io/micronaut/http/MediaTypeUtils.java deleted file mode 100644 index 45a281626c9..00000000000 --- a/http/src/main/java/io/micronaut/http/MediaTypeUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2017-2024 original authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.micronaut.http; - -import io.micronaut.core.annotation.NonNull; - -/** - * Utility methods for working with {@link MediaType}. - * @author Sergio del Amo - * @since 4.7.0 - */ -public final class MediaTypeUtils { - - /** - * - * @param mediaType Media Type - * @return Returns true if the media type is {@link MediaType#APPLICATION_JSON_TYPE}, {@link MediaType#TEXT_JSON_TYPE}, {@link MediaType#APPLICATION_HAL_JSON_TYPE}, {@link MediaType#APPLICATION_JSON_GITHUB_TYPE}, {@link MediaType#APPLICATION_JSON_FEED_TYPE}, {@link {@link MediaType#APPLICATION_JSON_PROBLEM_TYPE}, {@link MediaType#APPLICATION_JSON_PATCH_TYPE}, {@link MediaType#APPLICATION_JSON_MERGE_PATCH_TYPE} or {@link MediaType#APPLICATION_JSON_SCHEMA_TYPE}. - */ - public static boolean isJson(@NonNull MediaType mediaType) { - return mediaType.equals(MediaType.APPLICATION_JSON_TYPE) - || mediaType.equals(MediaType.TEXT_JSON_TYPE) - || mediaType.equals(MediaType.APPLICATION_HAL_JSON_TYPE) - || mediaType.equals(MediaType.APPLICATION_JSON_GITHUB_TYPE) - || mediaType.equals(MediaType.APPLICATION_JSON_FEED_TYPE) - || mediaType.equals(MediaType.APPLICATION_JSON_PROBLEM_TYPE) - || mediaType.equals(MediaType.APPLICATION_JSON_PATCH_TYPE) - || mediaType.equals(MediaType.APPLICATION_JSON_MERGE_PATCH_TYPE) - || mediaType.equals(MediaType.APPLICATION_JSON_SCHEMA_TYPE); - - } -} diff --git a/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java b/http/src/test/java/io/micronaut/http/MediaTypeTest.java similarity index 92% rename from http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java rename to http/src/test/java/io/micronaut/http/MediaTypeTest.java index 32ad96bc481..92761af06f5 100644 --- a/http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java +++ b/http/src/test/java/io/micronaut/http/MediaTypeTest.java @@ -8,11 +8,11 @@ import static io.micronaut.http.MediaType.*; import static org.junit.jupiter.api.Assertions.*; -class MediaTypeUtilsTest { +class MediaTypeTest { @ParameterizedTest @MethodSource void isJsonTrue(MediaType mediaType) { - assertTrue(MediaTypeUtils.isJson(mediaType)); + assertTrue(mediaType.matchesAllOrWildcardOrExtension(MediaType.EXTENSION_JSON)); assertTrue(mediaType.matchesExtension(MediaType.EXTENSION_JSON)); } @@ -33,7 +33,7 @@ private static List isJsonTrue() { @ParameterizedTest @MethodSource void isJsonFalse(MediaType mediaType) { - assertFalse(MediaTypeUtils.isJson(mediaType)); + assertFalse(mediaType.matchesExtension(MediaType.EXTENSION_JSON)); } private static List isJsonFalse() { diff --git a/json-core/src/main/java/io/micronaut/json/body/JsonMessageHandler.java b/json-core/src/main/java/io/micronaut/json/body/JsonMessageHandler.java index 8111fe6e770..c04588bee46 100644 --- a/json-core/src/main/java/io/micronaut/json/body/JsonMessageHandler.java +++ b/json-core/src/main/java/io/micronaut/json/body/JsonMessageHandler.java @@ -85,7 +85,7 @@ public JsonMapper getJsonMapper() { @Override public boolean isReadable(@NonNull Argument type, MediaType mediaType) { - return mediaType != null && mediaType.matchesExtension(MediaType.EXTENSION_JSON); + return mediaType != null && mediaType.matchesAllOrWildcardOrExtension(MediaType.EXTENSION_JSON); } private static CodecException decorateRead(Argument type, IOException e) { @@ -122,7 +122,7 @@ public T read(@NonNull Argument type, MediaType mediaType, @NonNull Headers h @Override public boolean isWriteable(@NonNull Argument type, MediaType mediaType) { - return mediaType != null && mediaType.matchesExtension(MediaType.EXTENSION_JSON); + return mediaType != null && mediaType.matchesAllOrWildcardOrExtension(MediaType.EXTENSION_JSON); } private static CodecException decorateWrite(Object object, IOException e) { From 3f95b0cb0d3dababfa22c13606450363037f3fc4 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 12:35:20 +0200 Subject: [PATCH 20/29] Add HtmlSanitizer --- .../DefaultHtmlErrorResponseBodyProvider.java | 11 ++-- .../util/HtmlEntityEncodingHtmlSanitizer.java | 56 +++++++++++++++++++ .../io/micronaut/http/util/HtmlSanitizer.java | 35 ++++++++++++ .../HtmlEntityEncodingHtmlSanitizerTest.java | 50 +++++++++++++++++ 4 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java create mode 100644 http/src/main/java/io/micronaut/http/util/HtmlSanitizer.java create mode 100644 http/src/test/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizerTest.java diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java index 2c8ece14419..3fc4a41b672 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java @@ -22,6 +22,7 @@ import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; +import io.micronaut.http.util.HtmlSanitizer; import jakarta.inject.Singleton; import java.text.MessageFormat; @@ -122,21 +123,23 @@ final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBod } """; + private final HtmlSanitizer htmlSanitizer; private final MessageSource messageSource; private final LocaleResolver> localeResolver; private final Map cache = new ConcurrentHashMap<>(); - DefaultHtmlErrorResponseBodyProvider(MessageSource messageSource, + DefaultHtmlErrorResponseBodyProvider(HtmlSanitizer htmlSanitizer, + MessageSource messageSource, LocaleResolver> localeResolver) { + this.htmlSanitizer = htmlSanitizer; this.messageSource = messageSource; this.localeResolver = localeResolver; } @Override public String body(@NonNull ErrorContext errorContext, @NonNull HttpResponse response) { - final HttpStatus status = response.status(); int httpStatusCode = response.code(); - String httpStatusReason = response.reason(); + String httpStatusReason = htmlSanitizer.sanitize(response.reason()); Locale locale = localeResolver.resolveOrDefault(errorContext.getRequest()); return cache.computeIfAbsent(new LocaleStatus(locale, httpStatusCode), key -> html(locale, httpStatusCode, httpStatusReason, errorContext)); } @@ -171,7 +174,7 @@ private String article(Locale locale, for (io.micronaut.http.server.exceptions.response.Error e : errorContext.getErrors()) { if (!e.getMessage().equalsIgnoreCase(httpStatusReason)) { - sb.append(e.getMessage()); + sb.append(htmlSanitizer.sanitize(e.getMessage())); sb.append("
"); } } diff --git a/http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java b/http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java new file mode 100644 index 00000000000..17a19308373 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.util; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.NonNull; +import jakarta.annotation.Nullable; +import jakarta.inject.Singleton; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Given an HTML string, it encodes the following characters: {@code &} to {@code &}, {@code <} to {@code <}, {@code >} to {@code >}, {@code "} to {@code "}, and {@code '} to {@code '}. + * @see Cross site Scripting Prevention Cheat Sheet + */ +@Singleton +@Requires(missingBeans = HtmlSanitizer.class) +public class HtmlEntityEncodingHtmlSanitizer implements HtmlSanitizer { + private final Map encodedMap; + + public HtmlEntityEncodingHtmlSanitizer() { + encodedMap = new LinkedHashMap<>(); + encodedMap.put("&", "&"); + encodedMap.put("<", "<"); + encodedMap.put(">", ">"); + encodedMap.put("\"", """); + encodedMap.put("'", "'"); + } + + @Override + @NonNull + public String sanitize(@Nullable String html) { + if (html == null) { + return ""; + } + String sanitized = html; + for (Map.Entry entry : encodedMap.entrySet()) { + sanitized = sanitized.replaceAll(entry.getKey(), entry.getValue()); + } + return sanitized; + } +} diff --git a/http/src/main/java/io/micronaut/http/util/HtmlSanitizer.java b/http/src/main/java/io/micronaut/http/util/HtmlSanitizer.java new file mode 100644 index 00000000000..54494c97562 --- /dev/null +++ b/http/src/main/java/io/micronaut/http/util/HtmlSanitizer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.util; + +import io.micronaut.core.annotation.NonNull; +import jakarta.annotation.Nullable; + +/** + * API to sanitize a String of HTML. + * @author Sergio del Amo + * @since 4.7.0 + */ +@FunctionalInterface +public interface HtmlSanitizer { + /** + * Sanitizes a string of HTML. + * @param html the String of HTML to Sanitize + * @return a sanitized version of the supplied HTML String. + */ + @NonNull + String sanitize(@Nullable String html); +} diff --git a/http/src/test/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizerTest.java b/http/src/test/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizerTest.java new file mode 100644 index 00000000000..7b44edf578a --- /dev/null +++ b/http/src/test/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizerTest.java @@ -0,0 +1,50 @@ +package io.micronaut.http.util; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.annotation.Requires; +import jakarta.inject.Singleton; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class HtmlEntityEncodingHtmlSanitizerTest { + @Test + void sanitize() { + HtmlEntityEncodingHtmlSanitizer sanitizer = new HtmlEntityEncodingHtmlSanitizer(); + String html = sanitizer.sanitize("Hello, World!"); + assertEquals("<b>Hello, World!</b>", html); + + html = sanitizer.sanitize("\"Hello, World!\""); + assertEquals(""Hello, World!"", html); + html = sanitizer.sanitize("'Hello, World!'"); + assertEquals("'Hello, World!'", html); + assertEquals("", sanitizer.sanitize(null)); + } + + @Test + void beanOfHtmlSanitizerExistsAndItDefaultsToHtmlEntityEncodingHtmlSanitizer() { + try (ApplicationContext ctx = ApplicationContext.run()) { + assertTrue(ctx.containsBean(HtmlSanitizer.class)); + assertTrue(ctx.getBean(HtmlSanitizer.class) instanceof HtmlEntityEncodingHtmlSanitizer); + } + } + + @Test + void itIsEasyToProvideYourOwnBeanOfTypeHtmlSanitizer() { + try (ApplicationContext ctx = ApplicationContext.run(Map.of("spec.name", "HtmlSanitizerReplacement"))) { + assertTrue(ctx.containsBean(HtmlSanitizer.class)); + assertTrue(ctx.getBean(HtmlSanitizer.class) instanceof BogusHtmlSanitizer); + } + } + + @Singleton + @Requires(property = "spec.name", value = "HtmlSanitizerReplacement") + static class BogusHtmlSanitizer implements HtmlSanitizer { + @Override + public String sanitize(String html) { + return "Bogus"; + } + } +} From 23fc57d3f6be8a3afe4fb1537213321a10a026ba Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 12:35:26 +0200 Subject: [PATCH 21/29] fix test --- .../response/HtmlErrorResponseBodyProviderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java index 3096dc1b4f2..fc5ea273cf6 100644 --- a/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java +++ b/http-server/src/test/java/io/micronaut/http/server/exceptions/response/HtmlErrorResponseBodyProviderTest.java @@ -53,12 +53,12 @@ public HttpStatus getStatus() { @Override public int code() { - return 0; + return status.getCode(); } @Override public String reason() { - return ""; + return status.getReason(); } @Override From 9ad37a7c7bae76e5ac9575a3a441fff43404701b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 12:50:00 +0200 Subject: [PATCH 22/29] add @Requires missingBeans --- .../response/DefaultHtmlErrorResponseBodyProvider.java | 2 ++ .../response/DefaultJsonErrorResponseBodyProvider.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java index 3fc4a41b672..8ffb5bfe218 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java @@ -16,6 +16,7 @@ package io.micronaut.http.server.exceptions.response; import io.micronaut.context.MessageSource; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.util.LocaleResolver; @@ -39,6 +40,7 @@ */ @Internal @Singleton +@Requires(missingBeans = HtmlErrorResponseBodyProvider.class) final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBodyProvider { private static final Map DEFAULT_ERROR_BOLD = Map.of( NOT_FOUND.getCode(), "The page you were looking for doesn’t exist", diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java index e6981568db4..d478ba0a427 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultJsonErrorResponseBodyProvider.java @@ -15,6 +15,7 @@ */ package io.micronaut.http.server.exceptions.response; +import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.Internal; import io.micronaut.http.HttpResponse; import io.micronaut.http.hateoas.JsonError; @@ -33,6 +34,7 @@ */ @Internal @Singleton +@Requires(missingBeans = JsonErrorResponseBodyProvider.class) final class DefaultJsonErrorResponseBodyProvider implements JsonErrorResponseBodyProvider { private final boolean alwaysSerializeErrorsAsList; From b61a6d7aa252a6b319066f71f27e5817f2a4347c Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 12:50:12 +0200 Subject: [PATCH 23/29] improve docs --- .../errorHandling/errorFormatting.adoc | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc b/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc index 05b97cf6d19..d6d8f5e1804 100644 --- a/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc +++ b/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc @@ -1,5 +1,17 @@ -The Micronaut framework produces error response bodies via beans of type api:http.server.exceptions.response.ErrorResponseProcessor[]. +The Micronaut framework produces error responses via a bean of type api:http.server.exceptions.response.ErrorResponseProcessor[]. -The default response body is link:https://github.com/blongden/vnd.error[vnd.error], however you can create your own implementation of type api:http.server.exceptions.response.ErrorResponseProcessor[] to control the responses. +JSON error responses are provided with a bean of type api:http.server.exceptions.response.JsonErrorResponseBodyProvider[]. +The default implementation api:http.server.exceptions.response.DefaultJsonErrorResponseBodyProvider[] outputs link:https://github.com/blongden/vnd.error[vnd.error] responses. -If customization of the response other than items related to the errors is desired, the exception handler that is handling the exception needs to be overridden. +HTML error responses are provided via a bean of type api:http.server.exceptions.response.HtmlErrorResponseBodyProvider[]. +The default implementation api:http.server.exceptions.response.DefaultHtmlErrorResponseBodyProvider[] outputs HTML which <> with codes such as: +`.error.bold`, `.error.title`, `.error`. For example, you could localize the default 404 error page into Spanish: + +[source,properties] +---- +404.error.bold=La página que buscabas no existe +404.error.title=No encontrado +404.error=Es posible que haya escrito mal la dirección o que la página se haya movido. +---- + +If customization of the response other than items related to the errors is desired, the <> that is handling the exception needs to be overridden. From 6757e006a0c6575965bae652427f3958844ecb3b Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 12:52:13 +0200 Subject: [PATCH 24/29] remove javadoc of an exception not thrown --- .../io/micronaut/function/executor/FunctionInitializer.java | 2 -- .../docs/guide/httpServer/errorHandling/errorFormatting.adoc | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java b/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java index 9d30f57e2bf..ff29ad60a0d 100644 --- a/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java +++ b/function/src/main/java/io/micronaut/function/executor/FunctionInitializer.java @@ -26,7 +26,6 @@ import io.micronaut.http.codec.MediaTypeCodecRegistry; import io.micronaut.inject.annotation.MutableAnnotationMetadata; -import java.io.IOException; import java.util.Collections; import java.util.function.Function; @@ -96,7 +95,6 @@ public void close() { * * @param args The arguments passed to main * @param supplier The function that executes this function - * @throws IOException If an error occurs */ public void run(String[] args, Function supplier) { ApplicationContext applicationContext = this.applicationContext; diff --git a/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc b/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc index d6d8f5e1804..b8b99362d17 100644 --- a/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc +++ b/src/main/docs/guide/httpServer/errorHandling/errorFormatting.adoc @@ -1,10 +1,10 @@ The Micronaut framework produces error responses via a bean of type api:http.server.exceptions.response.ErrorResponseProcessor[]. JSON error responses are provided with a bean of type api:http.server.exceptions.response.JsonErrorResponseBodyProvider[]. -The default implementation api:http.server.exceptions.response.DefaultJsonErrorResponseBodyProvider[] outputs link:https://github.com/blongden/vnd.error[vnd.error] responses. +The default implementation outputs link:https://github.com/blongden/vnd.error[vnd.error] responses. HTML error responses are provided via a bean of type api:http.server.exceptions.response.HtmlErrorResponseBodyProvider[]. -The default implementation api:http.server.exceptions.response.DefaultHtmlErrorResponseBodyProvider[] outputs HTML which <> with codes such as: +The default implementation outputs HTML which <> with codes such as: `.error.bold`, `.error.title`, `.error`. For example, you could localize the default 404 error page into Spanish: [source,properties] From 8155278e347d92e1ddb804b69e20733db6d4b52e Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 13:04:50 +0200 Subject: [PATCH 25/29] remove trailing space --- .../response/DefaultHtmlErrorResponseBodyProviderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java index 12d15963d4d..328d67c794b 100644 --- a/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java +++ b/http-server-netty/src/test/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProviderTest.java @@ -129,6 +129,6 @@ MessageSource createMessageSource() { } @Introspected - record Book(@NotBlank String title, @NotBlank String author, @Max(4032 ) int pages) { + record Book(@NotBlank String title, @NotBlank String author, @Max(4032) int pages) { } } From 0522e847e3a29de7ad23422005af285a4962c958 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 13:07:42 +0200 Subject: [PATCH 26/29] remove @Singleton annotation --- .../exceptions/response/JsonErrorResponseBodyProvider.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonErrorResponseBodyProvider.java index 53660e31354..0a7d6cc0369 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/JsonErrorResponseBodyProvider.java @@ -16,7 +16,6 @@ package io.micronaut.http.server.exceptions.response; import io.micronaut.http.MediaType; -import jakarta.inject.Singleton; /** * A {@link ErrorResponseBodyProvider} for JSON responses. @@ -25,7 +24,7 @@ * @since 4.7.0 * @param The error type */ -@Singleton +@FunctionalInterface public interface JsonErrorResponseBodyProvider extends ErrorResponseBodyProvider { @Override default String contentType() { From fb3590d40d8f85cca0e584910f0792ee231d4043 Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 13:09:06 +0200 Subject: [PATCH 27/29] remove extra whitespace --- .../http/util/HtmlEntityEncodingHtmlSanitizer.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java b/http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java index 17a19308373..57137f8ef3e 100644 --- a/http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java +++ b/http/src/main/java/io/micronaut/http/util/HtmlEntityEncodingHtmlSanitizer.java @@ -34,11 +34,11 @@ public class HtmlEntityEncodingHtmlSanitizer implements HtmlSanitizer { public HtmlEntityEncodingHtmlSanitizer() { encodedMap = new LinkedHashMap<>(); - encodedMap.put("&", "&"); - encodedMap.put("<", "<"); - encodedMap.put(">", ">"); - encodedMap.put("\"", """); - encodedMap.put("'", "'"); + encodedMap.put("&", "&"); + encodedMap.put("<", "<"); + encodedMap.put(">", ">"); + encodedMap.put("\"", """); + encodedMap.put("'", "'"); } @Override From dfb9d7542e5c06e3f0e81ec13b47ec3c289d7bbe Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 13:23:34 +0200 Subject: [PATCH 28/29] simpler font-family --- .../response/DefaultHtmlErrorResponseBodyProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java index 8ffb5bfe218..f02297834d7 100644 --- a/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java +++ b/http-server/src/main/java/io/micronaut/http/server/exceptions/response/DefaultHtmlErrorResponseBodyProvider.java @@ -71,7 +71,7 @@ final class DefaultHtmlErrorResponseBodyProvider implements HtmlErrorResponseBod background: #2559a7; color: #FFF; display: grid; - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: -apple-system, "Helvetica Neue", Helvetica, sans-serif; font-size: clamp(1rem, 2.5vw, 2rem); -webkit-font-smoothing: antialiased; font-style: normal; From 91ebabdd42ead98eb790f3d480f06e860318c1cb Mon Sep 17 00:00:00 2001 From: Sergio del Amo Date: Wed, 2 Oct 2024 15:22:47 +0200 Subject: [PATCH 29/29] fix test --- http/src/test/java/io/micronaut/http/MediaTypeTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/http/src/test/java/io/micronaut/http/MediaTypeTest.java b/http/src/test/java/io/micronaut/http/MediaTypeTest.java index 92761af06f5..db7d36dffc8 100644 --- a/http/src/test/java/io/micronaut/http/MediaTypeTest.java +++ b/http/src/test/java/io/micronaut/http/MediaTypeTest.java @@ -26,8 +26,9 @@ private static List isJsonTrue() { APPLICATION_JSON_PROBLEM_TYPE, APPLICATION_JSON_PATCH_TYPE, APPLICATION_JSON_MERGE_PATCH_TYPE, - APPLICATION_JSON_SCHEMA_TYPE - ); + APPLICATION_JSON_SCHEMA_TYPE, + APPLICATION_VND_ERROR_TYPE + ); } @ParameterizedTest @@ -44,7 +45,6 @@ private static List isJsonFalse() { APPLICATION_YAML_TYPE, APPLICATION_HAL_XML_TYPE, APPLICATION_ATOM_XML_TYPE, - APPLICATION_VND_ERROR_TYPE, APPLICATION_JSON_STREAM_TYPE, APPLICATION_OCTET_STREAM_TYPE, APPLICATION_GRAPHQL_TYPE,