Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: nicer error pages for HTML responses #11210

Merged
merged 30 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e6ac16c
feat: Improved HTML Error pages
sdelamo Sep 27, 2024
b057c74
remove unused import
sdelamo Sep 27, 2024
ad1e55b
remove svgs
sdelamo Sep 27, 2024
a36464e
rename to error response body provider
sdelamo Sep 27, 2024
845e94a
extract request into a local variable and reuse
sdelamo Sep 27, 2024
5fd2b03
flip the condition
sdelamo Sep 27, 2024
4ded5ae
make class final
sdelamo Sep 27, 2024
0136f34
use @Requires missing beans instead of Secondary
sdelamo Sep 27, 2024
a50ca6d
fill empty method
sdelamo Sep 27, 2024
6f5b14c
Update http-server-netty/src/test/resources/logback.xml
sdelamo Sep 30, 2024
a288899
Update http-server/src/main/java/io/micronaut/http/server/exceptions/…
sdelamo Sep 30, 2024
de6f89c
initialize arraylist with size
sdelamo Sep 30, 2024
031a604
javadoc: add missing javadoc
sdelamo Sep 30, 2024
810bc55
check request does not accept json
sdelamo Sep 30, 2024
6918fc3
for HtmlErrorResponseBodyProvider specify generic as String
sdelamo Sep 30, 2024
810eb8e
don’t use generic for HtmlErrorResponseBodyProvider
sdelamo Sep 30, 2024
4b7c7d1
test matchesExtension
sdelamo Sep 30, 2024
27a15c3
Use HttpResponse:code() and HttpResponse::reason()
sdelamo Sep 30, 2024
6d63118
use MediaType::matchesExtensions
sdelamo Oct 2, 2024
3f95b0c
Add HtmlSanitizer
sdelamo Oct 2, 2024
23fc57d
fix test
sdelamo Oct 2, 2024
9ad37a7
add @Requires missingBeans
sdelamo Oct 2, 2024
b61a6d7
improve docs
sdelamo Oct 2, 2024
14eee77
Merge branch '4.7.x' into error-body-processor
sdelamo Oct 2, 2024
6757e00
remove javadoc of an exception not thrown
sdelamo Oct 2, 2024
8155278
remove trailing space
sdelamo Oct 2, 2024
0522e84
remove @Singleton annotation
sdelamo Oct 2, 2024
fb3590d
remove extra whitespace
sdelamo Oct 2, 2024
dfb9d75
simpler font-family
sdelamo Oct 2, 2024
91ebabd
fix test
sdelamo Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> jsonOptional = ex.getResponse().getBody(String.class);
assertTrue(jsonOptional.isPresent());
String json = jsonOptional.get();
assertFalse(json.contains("<!doctype html>"));
}

@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<String> htmlOptional = ex.getResponse().getBody(String.class);
assertTrue(htmlOptional.isPresent());
String html = htmlOptional.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -50,7 +47,10 @@ public MutableHttpResponse processResponse(ErrorContext errorContext, MutableHtt
return (MutableHttpResponse<JsonError>) 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());
}
Expand Down
1 change: 1 addition & 0 deletions http/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
testImplementation project(":runtime")
testImplementation(libs.logback.classic)
testImplementation(libs.jazzer.junit)
testImplementation(libs.junit.jupiter.params)
}

tasks.named("compileKotlin") {
Expand Down
44 changes: 44 additions & 0 deletions http/src/main/java/io/micronaut/http/MediaTypeUtils.java
Original file line number Diff line number Diff line change
@@ -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) {
sdelamo marked this conversation as resolved.
Show resolved Hide resolved
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);

}
}
85 changes: 85 additions & 0 deletions http/src/test/java/io/micronaut/http/MediaTypeUtilsTest.java
Original file line number Diff line number Diff line change
@@ -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<MediaType> 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<MediaType> 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);
}
}
Loading