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:
Phillip Webb 2024-06-14 14:33:11 -07:00
parent 1f698d8ea2
commit 60b7e6cf23
9 changed files with 200 additions and 54 deletions

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"); * 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;
} }
/** /**

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"); * 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;
}
}
} }

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"); * 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());

View File

@ -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);
}
};
}
}
} }

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"); * 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;
}
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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);

View File

@ -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");
}
} }