Merge branch '3.2.x'

Closes gh-41141
This commit is contained in:
Phillip Webb
2024-06-17 16:51:53 -07:00
9 changed files with 199 additions and 59 deletions

View File

@@ -35,6 +35,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RequestPredicate;
@@ -91,6 +92,8 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private static final ErrorAttributeOptions ONLY_STATUS = ErrorAttributeOptions.of(Include.STATUS);
private final ErrorProperties errorProperties;
/**
@@ -118,13 +121,13 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
* @return a {@code Publisher} of the HTTP response
*/
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {
Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
int errorStatus = getHttpStatus(error);
ServerResponse.BodyBuilder responseBody = ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8);
return Flux.just(getData(errorStatus).toArray(new String[] {}))
.flatMap((viewName) -> renderErrorView(viewName, responseBody, error))
int status = getHttpStatus(getErrorAttributes(request, ONLY_STATUS));
Map<String, Object> errorAttributes = getErrorAttributes(request, MediaType.TEXT_HTML);
ServerResponse.BodyBuilder responseBody = ServerResponse.status(status).contentType(TEXT_HTML_UTF8);
return Flux.just(getData(status).toArray(new String[] {}))
.flatMap((viewName) -> renderErrorView(viewName, responseBody, errorAttributes))
.switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled()
? renderDefaultErrorView(responseBody, error) : Mono.error(getError(request)))
? renderDefaultErrorView(responseBody, errorAttributes) : Mono.error(getError(request)))
.next();
}
@@ -145,10 +148,15 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
* @return a {@code Publisher} of the HTTP response
*/
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return ServerResponse.status(getHttpStatus(error))
int status = getHttpStatus(getErrorAttributes(request, ONLY_STATUS));
Map<String, Object> errorAttributes = getErrorAttributes(request, MediaType.ALL);
return ServerResponse.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(error));
.body(BodyInserters.fromValue(errorAttributes));
}
private Map<String, Object> getErrorAttributes(ServerRequest request, MediaType mediaType) {
return getErrorAttributes(request, getErrorAttributeOptions(request, mediaType));
}
protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) {
@@ -232,7 +240,9 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
* @return the error HTTP status
*/
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get("status");
Object status = errorAttributes.get("status");
Assert.state(status instanceof Integer, "ErrorAttributes must contain a status integer");
return (int) status;
}
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,9 +25,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
@@ -36,12 +39,17 @@ import org.springframework.boot.test.context.runner.ReactiveWebApplicationContex
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.test.web.reactive.server.HttpHandlerConnector.FailureAfterResponseCompletedException;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
@@ -50,6 +58,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
@@ -573,6 +582,21 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
});
}
@Test
void customErrorWebExceptionHandlerWithoutStatus() {
this.contextRunner.withUserConfiguration(CustomErrorWebExceptionHandlerWithoutStatus.class).run((context) -> {
WebTestClient client = getWebClient(context);
client.get()
.uri("/badRequest")
.exchange()
.expectStatus()
.isBadRequest()
.expectBody()
.jsonPath("status")
.doesNotExist();
});
}
private String getErrorTemplatesLocation() {
String packageName = getClass().getPackage().getName();
return "classpath:/" + packageName.replace('.', '/') + "/templates/";
@@ -675,4 +699,29 @@ class DefaultErrorWebExceptionHandlerIntegrationTests {
}
static class CustomErrorWebExceptionHandlerWithoutStatus {
@Bean
@Order(-1)
ErrorWebExceptionHandler errorWebExceptionHandler(ServerProperties serverProperties,
ErrorAttributes errorAttributes, WebProperties webProperties,
ObjectProvider<ViewResolver> viewResolvers, ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes,
webProperties.getResources(), serverProperties.getError(), applicationContext) {
@Override
protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) {
return super.getErrorAttributeOptions(request, mediaType).excluding(Include.STATUS, Include.ERROR);
}
};
exceptionHandler.setViewResolvers(viewResolvers.orderedStream().toList());
exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@ package org.springframework.boot.autoconfigure.web.reactive.error;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
@@ -55,7 +54,7 @@ class DefaultErrorWebExceptionHandlerTests {
@Test
void nonStandardErrorStatusCodeShouldNotFail() {
ErrorAttributes errorAttributes = mock(ErrorAttributes.class);
given(errorAttributes.getErrorAttributes(any(), any())).willReturn(getErrorAttributes());
given(errorAttributes.getErrorAttributes(any(), any())).willReturn(Collections.singletonMap("status", 498));
Resources resourceProperties = new Resources();
ErrorProperties errorProperties = new ErrorProperties();
ApplicationContext context = new AnnotationConfigReactiveWebApplicationContext();
@@ -67,10 +66,6 @@ class DefaultErrorWebExceptionHandlerTests {
exceptionHandler.handle(exchange, new RuntimeException()).block();
}
private Map<String, Object> getErrorAttributes() {
return Collections.singletonMap("status", 498);
}
private void setupViewResolver(DefaultErrorWebExceptionHandler exceptionHandler) {
View view = mock(View.class);
given(view.render(any(), any(), any())).willReturn(Mono.empty());

View File

@@ -35,15 +35,20 @@ import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -343,6 +348,18 @@ class BasicErrorControllerIntegrationTests {
assertThat(entity.getBody()).isNull();
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void customErrorControllerWithoutStatusConfiguration() {
load(CustomErrorControllerWithoutStatusConfiguration.class);
RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation")))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body("{}");
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
assertThat(entity.getBody()).doesNotContainKey("status");
}
private void assertErrorAttributes(Map<?, ?> content, String status, String error, Class<?> exception,
String message, String path) {
assertThat(content.get("status")).as("Wrong status").hasToString(status);
@@ -363,12 +380,16 @@ class BasicErrorControllerIntegrationTests {
}
private void load(String... arguments) {
load(TestConfiguration.class, arguments);
}
private void load(Class<?> configuration, String... arguments) {
List<String> args = new ArrayList<>();
args.add("--server.port=0");
if (arguments != null) {
args.addAll(Arrays.asList(arguments));
}
this.context = SpringApplication.run(TestConfiguration.class, StringUtils.toStringArray(args));
this.context = SpringApplication.run(configuration, StringUtils.toStringArray(args));
}
@Target(ElementType.TYPE)
@@ -394,11 +415,13 @@ class BasicErrorControllerIntegrationTests {
@Bean
View error() {
return new AbstractView() {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().write("ERROR_BEAN");
}
};
}
@@ -498,4 +521,23 @@ class BasicErrorControllerIntegrationTests {
}
static class CustomErrorControllerWithoutStatusConfiguration extends TestConfiguration {
@Bean
BasicErrorController basicErrorController(ServerProperties serverProperties, ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, serverProperties.getError(),
errorViewResolvers.orderedStream().toList()) {
@Override
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request,
MediaType mediaType) {
return super.getErrorAttributeOptions(request, mediaType).excluding(Include.STATUS);
}
};
}
}
}