mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-07-15 01:07:30 +08:00
Allow 'status' and 'error' to be excluded from error response
Update `ErrorAttributeOptions` to allow the `status` and `error` fields to be excluded from the response without throwing a NullPointerException. Fixes gh-30011
This commit is contained in:
parent
1f698d8ea2
commit
60b7e6cf23
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -35,6 +35,7 @@ import org.springframework.context.ApplicationContext;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.InvalidMediaTypeException;
|
import org.springframework.http.InvalidMediaTypeException;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
import org.springframework.util.MimeTypeUtils;
|
||||||
import org.springframework.web.reactive.function.BodyInserters;
|
import org.springframework.web.reactive.function.BodyInserters;
|
||||||
import org.springframework.web.reactive.function.server.RequestPredicate;
|
import org.springframework.web.reactive.function.server.RequestPredicate;
|
||||||
@ -90,6 +91,8 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
|
|||||||
SERIES_VIEWS = Collections.unmodifiableMap(views);
|
SERIES_VIEWS = Collections.unmodifiableMap(views);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final ErrorAttributeOptions ONLY_STATUS = ErrorAttributeOptions.of(Include.STATUS);
|
||||||
|
|
||||||
private final ErrorProperties errorProperties;
|
private final ErrorProperties errorProperties;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,13 +120,13 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
|
|||||||
* @return a {@code Publisher} of the HTTP response
|
* @return a {@code Publisher} of the HTTP response
|
||||||
*/
|
*/
|
||||||
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {
|
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {
|
||||||
Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
|
int status = getHttpStatus(getErrorAttributes(request, ONLY_STATUS));
|
||||||
int errorStatus = getHttpStatus(error);
|
Map<String, Object> errorAttributes = getErrorAttributes(request, MediaType.TEXT_HTML);
|
||||||
ServerResponse.BodyBuilder responseBody = ServerResponse.status(errorStatus).contentType(TEXT_HTML_UTF8);
|
ServerResponse.BodyBuilder responseBody = ServerResponse.status(status).contentType(TEXT_HTML_UTF8);
|
||||||
return Flux.just(getData(errorStatus).toArray(new String[] {}))
|
return Flux.just(getData(status).toArray(new String[] {}))
|
||||||
.flatMap((viewName) -> renderErrorView(viewName, responseBody, error))
|
.flatMap((viewName) -> renderErrorView(viewName, responseBody, errorAttributes))
|
||||||
.switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled()
|
.switchIfEmpty(this.errorProperties.getWhitelabel().isEnabled()
|
||||||
? renderDefaultErrorView(responseBody, error) : Mono.error(getError(request)))
|
? renderDefaultErrorView(responseBody, errorAttributes) : Mono.error(getError(request)))
|
||||||
.next();
|
.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,10 +147,15 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
|
|||||||
* @return a {@code Publisher} of the HTTP response
|
* @return a {@code Publisher} of the HTTP response
|
||||||
*/
|
*/
|
||||||
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
|
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
|
||||||
Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
|
int status = getHttpStatus(getErrorAttributes(request, ONLY_STATUS));
|
||||||
return ServerResponse.status(getHttpStatus(error))
|
Map<String, Object> errorAttributes = getErrorAttributes(request, MediaType.ALL);
|
||||||
|
return ServerResponse.status(status)
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.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) {
|
protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) {
|
||||||
@ -215,7 +223,9 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
|
|||||||
* @return the error HTTP status
|
* @return the error HTTP status
|
||||||
*/
|
*/
|
||||||
protected int getHttpStatus(Map<String, Object> errorAttributes) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
|
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.HttpHandlerAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
|
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.CapturedOutput;
|
||||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
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.DefaultErrorAttributes;
|
||||||
import org.springframework.boot.web.reactive.error.ErrorAttributes;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
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.HttpHandlerConnector.FailureAfterResponseCompletedException;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
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.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
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.ResponseStatusException;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.WebFilter;
|
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() {
|
private String getErrorTemplatesLocation() {
|
||||||
String packageName = getClass().getPackage().getName();
|
String packageName = getClass().getPackage().getName();
|
||||||
return "classpath:/" + packageName.replace('.', '/') + "/templates/";
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
@ -55,7 +54,7 @@ class DefaultErrorWebExceptionHandlerTests {
|
|||||||
@Test
|
@Test
|
||||||
void nonStandardErrorStatusCodeShouldNotFail() {
|
void nonStandardErrorStatusCodeShouldNotFail() {
|
||||||
ErrorAttributes errorAttributes = mock(ErrorAttributes.class);
|
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();
|
Resources resourceProperties = new Resources();
|
||||||
ErrorProperties errorProperties = new ErrorProperties();
|
ErrorProperties errorProperties = new ErrorProperties();
|
||||||
ApplicationContext context = new AnnotationConfigReactiveWebApplicationContext();
|
ApplicationContext context = new AnnotationConfigReactiveWebApplicationContext();
|
||||||
@ -67,10 +66,6 @@ class DefaultErrorWebExceptionHandlerTests {
|
|||||||
exceptionHandler.handle(exchange, new RuntimeException()).block();
|
exceptionHandler.handle(exchange, new RuntimeException()).block();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> getErrorAttributes() {
|
|
||||||
return Collections.singletonMap("status", 498);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setupViewResolver(DefaultErrorWebExceptionHandler exceptionHandler) {
|
private void setupViewResolver(DefaultErrorWebExceptionHandler exceptionHandler) {
|
||||||
View view = mock(View.class);
|
View view = mock(View.class);
|
||||||
given(view.render(any(), any(), any())).willReturn(Mono.empty());
|
given(view.render(any(), any(), any())).willReturn(Mono.empty());
|
||||||
|
@ -35,15 +35,20 @@ import jakarta.validation.constraints.NotNull;
|
|||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
|
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
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.DispatcherServletAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
|
||||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
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.ConfigurableApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -343,6 +348,18 @@ class BasicErrorControllerIntegrationTests {
|
|||||||
assertThat(entity.getBody()).isNull();
|
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,
|
private void assertErrorAttributes(Map<?, ?> content, String status, String error, Class<?> exception,
|
||||||
String message, String path) {
|
String message, String path) {
|
||||||
assertThat(content.get("status")).as("Wrong status").hasToString(status);
|
assertThat(content.get("status")).as("Wrong status").hasToString(status);
|
||||||
@ -363,12 +380,16 @@ class BasicErrorControllerIntegrationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void load(String... arguments) {
|
private void load(String... arguments) {
|
||||||
|
load(TestConfiguration.class, arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load(Class<?> configuration, String... arguments) {
|
||||||
List<String> args = new ArrayList<>();
|
List<String> args = new ArrayList<>();
|
||||||
args.add("--server.port=0");
|
args.add("--server.port=0");
|
||||||
if (arguments != null) {
|
if (arguments != null) {
|
||||||
args.addAll(Arrays.asList(arguments));
|
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)
|
@Target(ElementType.TYPE)
|
||||||
@ -394,11 +415,13 @@ class BasicErrorControllerIntegrationTests {
|
|||||||
@Bean
|
@Bean
|
||||||
View error() {
|
View error() {
|
||||||
return new AbstractView() {
|
return new AbstractView() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
|
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
|
||||||
HttpServletResponse response) throws Exception {
|
HttpServletResponse response) throws Exception {
|
||||||
response.getWriter().write("ERROR_BEAN");
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -20,6 +20,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -79,6 +80,19 @@ public final class ErrorAttributeOptions {
|
|||||||
return new ErrorAttributeOptions(Collections.unmodifiableSet(updated));
|
return new ErrorAttributeOptions(Collections.unmodifiableSet(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove elements from the given map if they are not included in this set of options.
|
||||||
|
* @param map the map to update
|
||||||
|
* @since 3.2.7
|
||||||
|
*/
|
||||||
|
public void retainIncluded(Map<String, Object> map) {
|
||||||
|
for (Include candidate : Include.values()) {
|
||||||
|
if (!this.includes.contains(candidate)) {
|
||||||
|
map.remove(candidate.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private EnumSet<Include> copyIncludes() {
|
private EnumSet<Include> copyIncludes() {
|
||||||
return (this.includes.isEmpty()) ? EnumSet.noneOf(Include.class) : EnumSet.copyOf(this.includes);
|
return (this.includes.isEmpty()) ? EnumSet.noneOf(Include.class) : EnumSet.copyOf(this.includes);
|
||||||
}
|
}
|
||||||
@ -88,7 +102,7 @@ public final class ErrorAttributeOptions {
|
|||||||
* @return an {@code ErrorAttributeOptions}
|
* @return an {@code ErrorAttributeOptions}
|
||||||
*/
|
*/
|
||||||
public static ErrorAttributeOptions defaults() {
|
public static ErrorAttributeOptions defaults() {
|
||||||
return of();
|
return of(Include.STATUS, Include.ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,22 +134,40 @@ public final class ErrorAttributeOptions {
|
|||||||
/**
|
/**
|
||||||
* Include the exception class name attribute.
|
* Include the exception class name attribute.
|
||||||
*/
|
*/
|
||||||
EXCEPTION,
|
EXCEPTION("exception"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Include the stack trace attribute.
|
* Include the stack trace attribute.
|
||||||
*/
|
*/
|
||||||
STACK_TRACE,
|
STACK_TRACE("trace"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Include the message attribute.
|
* Include the message attribute.
|
||||||
*/
|
*/
|
||||||
MESSAGE,
|
MESSAGE("message"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Include the binding errors attribute.
|
* Include the binding errors attribute.
|
||||||
*/
|
*/
|
||||||
BINDING_ERRORS
|
BINDING_ERRORS("errors"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include the HTTP status code.
|
||||||
|
* @since 3.2.7
|
||||||
|
*/
|
||||||
|
STATUS("status"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include the HTTP status code.
|
||||||
|
* @since 3.2.7
|
||||||
|
*/
|
||||||
|
ERROR("error");
|
||||||
|
|
||||||
|
private final String key;
|
||||||
|
|
||||||
|
Include(String key) {
|
||||||
|
this.key = key;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,18 +70,7 @@ public class DefaultErrorAttributes implements ErrorAttributes {
|
|||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
|
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
|
||||||
Map<String, Object> errorAttributes = getErrorAttributes(request, options.isIncluded(Include.STACK_TRACE));
|
Map<String, Object> errorAttributes = getErrorAttributes(request, options.isIncluded(Include.STACK_TRACE));
|
||||||
if (!options.isIncluded(Include.EXCEPTION)) {
|
options.retainIncluded(errorAttributes);
|
||||||
errorAttributes.remove("exception");
|
|
||||||
}
|
|
||||||
if (!options.isIncluded(Include.STACK_TRACE)) {
|
|
||||||
errorAttributes.remove("trace");
|
|
||||||
}
|
|
||||||
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
|
|
||||||
errorAttributes.remove("message");
|
|
||||||
}
|
|
||||||
if (!options.isIncluded(Include.BINDING_ERRORS)) {
|
|
||||||
errorAttributes.remove("errors");
|
|
||||||
}
|
|
||||||
return errorAttributes;
|
return errorAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,18 +91,7 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
|
|||||||
@Override
|
@Override
|
||||||
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
|
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
|
||||||
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
|
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
|
||||||
if (!options.isIncluded(Include.EXCEPTION)) {
|
options.retainIncluded(errorAttributes);
|
||||||
errorAttributes.remove("exception");
|
|
||||||
}
|
|
||||||
if (!options.isIncluded(Include.STACK_TRACE)) {
|
|
||||||
errorAttributes.remove("trace");
|
|
||||||
}
|
|
||||||
if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
|
|
||||||
errorAttributes.remove("message");
|
|
||||||
}
|
|
||||||
if (!options.isIncluded(Include.BINDING_ERRORS)) {
|
|
||||||
errorAttributes.remove("errors");
|
|
||||||
}
|
|
||||||
return errorAttributes;
|
return errorAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ class DefaultErrorAttributesTests {
|
|||||||
Exception error = new CustomException("Test Message");
|
Exception error = new CustomException("Test Message");
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, error),
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, error),
|
||||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
ErrorAttributeOptions.of(Include.MESSAGE, Include.STATUS, Include.ERROR));
|
||||||
assertThat(attributes).containsEntry("error", HttpStatus.I_AM_A_TEAPOT.getReasonPhrase());
|
assertThat(attributes).containsEntry("error", HttpStatus.I_AM_A_TEAPOT.getReasonPhrase());
|
||||||
assertThat(attributes).containsEntry("message", "Test Message");
|
assertThat(attributes).containsEntry("message", "Test Message");
|
||||||
assertThat(attributes).containsEntry("status", HttpStatus.I_AM_A_TEAPOT.value());
|
assertThat(attributes).containsEntry("status", HttpStatus.I_AM_A_TEAPOT.value());
|
||||||
@ -117,7 +117,7 @@ class DefaultErrorAttributesTests {
|
|||||||
Exception error = new Custom2Exception();
|
Exception error = new Custom2Exception();
|
||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, error),
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, error),
|
||||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
ErrorAttributeOptions.of(Include.MESSAGE, Include.STATUS, Include.ERROR));
|
||||||
assertThat(attributes).containsEntry("error", HttpStatus.I_AM_A_TEAPOT.getReasonPhrase());
|
assertThat(attributes).containsEntry("error", HttpStatus.I_AM_A_TEAPOT.getReasonPhrase());
|
||||||
assertThat(attributes).containsEntry("status", HttpStatus.I_AM_A_TEAPOT.value());
|
assertThat(attributes).containsEntry("status", HttpStatus.I_AM_A_TEAPOT.value());
|
||||||
assertThat(attributes).containsEntry("message", "Nope!");
|
assertThat(attributes).containsEntry("message", "Nope!");
|
||||||
@ -176,7 +176,7 @@ class DefaultErrorAttributesTests {
|
|||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||||
ServerRequest serverRequest = buildServerRequest(request, error);
|
ServerRequest serverRequest = buildServerRequest(request, error);
|
||||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(serverRequest,
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(serverRequest,
|
||||||
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE));
|
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE, Include.STATUS));
|
||||||
assertThat(attributes).containsEntry("status", 400);
|
assertThat(attributes).containsEntry("status", 400);
|
||||||
assertThat(attributes).containsEntry("message", "invalid request");
|
assertThat(attributes).containsEntry("message", "invalid request");
|
||||||
assertThat(attributes).containsEntry("exception", RuntimeException.class.getName());
|
assertThat(attributes).containsEntry("exception", RuntimeException.class.getName());
|
||||||
@ -191,7 +191,7 @@ class DefaultErrorAttributesTests {
|
|||||||
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||||
ServerRequest serverRequest = buildServerRequest(request, error);
|
ServerRequest serverRequest = buildServerRequest(request, error);
|
||||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(serverRequest,
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(serverRequest,
|
||||||
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE));
|
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE, Include.STATUS));
|
||||||
assertThat(attributes).containsEntry("status", 406);
|
assertThat(attributes).containsEntry("status", 406);
|
||||||
assertThat(attributes).containsEntry("message", "could not process request");
|
assertThat(attributes).containsEntry("message", "could not process request");
|
||||||
assertThat(attributes).containsEntry("exception", ResponseStatusException.class.getName());
|
assertThat(attributes).containsEntry("exception", ResponseStatusException.class.getName());
|
||||||
@ -283,6 +283,30 @@ class DefaultErrorAttributesTests {
|
|||||||
assertThat(attributes).doesNotContainKey("errors");
|
assertThat(attributes).doesNotContainKey("errors");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void excludeStatus() {
|
||||||
|
ResponseStatusException error = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE,
|
||||||
|
"could not process request");
|
||||||
|
this.errorAttributes = new DefaultErrorAttributes();
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||||
|
ServerRequest serverRequest = buildServerRequest(request, error);
|
||||||
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(serverRequest,
|
||||||
|
ErrorAttributeOptions.defaults().excluding(Include.STATUS));
|
||||||
|
assertThat(attributes).doesNotContainKey("status");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void excludeError() {
|
||||||
|
ResponseStatusException error = new ResponseStatusException(HttpStatus.NOT_ACCEPTABLE,
|
||||||
|
"could not process request");
|
||||||
|
this.errorAttributes = new DefaultErrorAttributes();
|
||||||
|
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
|
||||||
|
ServerRequest serverRequest = buildServerRequest(request, error);
|
||||||
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(serverRequest,
|
||||||
|
ErrorAttributeOptions.defaults().excluding(Include.ERROR));
|
||||||
|
assertThat(attributes).doesNotContainKey("error");
|
||||||
|
}
|
||||||
|
|
||||||
private ServerRequest buildServerRequest(MockServerHttpRequest request, Throwable error) {
|
private ServerRequest buildServerRequest(MockServerHttpRequest request, Throwable error) {
|
||||||
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
ServerWebExchange exchange = MockServerWebExchange.from(request);
|
||||||
this.errorAttributes.storeErrorInformation(error, exchange);
|
this.errorAttributes.storeErrorInformation(error, exchange);
|
||||||
|
@ -294,4 +294,20 @@ class DefaultErrorAttributesTests {
|
|||||||
assertThat(attributes).containsEntry("message", "custom message");
|
assertThat(attributes).containsEntry("message", "custom message");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void excludeStatus() {
|
||||||
|
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
|
||||||
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||||
|
ErrorAttributeOptions.defaults().excluding(Include.STATUS));
|
||||||
|
assertThat(attributes).doesNotContainKey("status");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void excludeError() {
|
||||||
|
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
|
||||||
|
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||||
|
ErrorAttributeOptions.defaults().excluding(Include.ERROR));
|
||||||
|
assertThat(attributes).doesNotContainKey("error");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user