From 60b7e6cf23c9befefa78bb41e1fb5073d2258de7 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 14 Jun 2024 14:33:11 -0700 Subject: [PATCH] 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 --- .../DefaultErrorWebExceptionHandler.java | 32 ++++++++---- ...orWebExceptionHandlerIntegrationTests.java | 51 ++++++++++++++++++- .../DefaultErrorWebExceptionHandlerTests.java | 9 +--- .../BasicErrorControllerIntegrationTests.java | 44 +++++++++++++++- .../boot/web/error/ErrorAttributeOptions.java | 44 +++++++++++++--- .../error/DefaultErrorAttributes.java | 13 +---- .../servlet/error/DefaultErrorAttributes.java | 13 +---- .../error/DefaultErrorAttributesTests.java | 32 ++++++++++-- .../error/DefaultErrorAttributesTests.java | 16 ++++++ 9 files changed, 200 insertions(+), 54 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java index 5bc52de7c76..072a049baa4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandler.java @@ -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. @@ -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; @@ -90,6 +91,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; /** @@ -117,13 +120,13 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa * @return a {@code Publisher} of the HTTP response */ protected Mono renderErrorView(ServerRequest request) { - Map 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 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(); } @@ -144,10 +147,15 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa * @return a {@code Publisher} of the HTTP response */ protected Mono renderErrorResponse(ServerRequest request) { - Map error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL)); - return ServerResponse.status(getHttpStatus(error)) + int status = getHttpStatus(getErrorAttributes(request, ONLY_STATUS)); + Map errorAttributes = getErrorAttributes(request, MediaType.ALL); + return ServerResponse.status(status) .contentType(MediaType.APPLICATION_JSON) - .body(BodyInserters.fromValue(error)); + .body(BodyInserters.fromValue(errorAttributes)); + } + + private Map getErrorAttributes(ServerRequest request, MediaType mediaType) { + return getErrorAttributes(request, getErrorAttributeOptions(request, mediaType)); } protected ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) { @@ -215,7 +223,9 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa * @return the error HTTP status */ protected int getHttpStatus(Map 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; } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java index 430ea1e7f11..8aec51f70eb 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerIntegrationTests.java @@ -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 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; + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java index 8662c71bedb..0d368313947 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/error/DefaultErrorWebExceptionHandlerTests.java @@ -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 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()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerIntegrationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerIntegrationTests.java index 691afe085d6..909e5908703 100755 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerIntegrationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/servlet/error/BasicErrorControllerIntegrationTests.java @@ -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 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 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 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 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); + } + + }; + } + + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java index c32f683276e..6b9bd4443c7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/error/ErrorAttributeOptions.java @@ -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. @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; +import java.util.Map; import java.util.Set; /** @@ -79,6 +80,19 @@ public final class ErrorAttributeOptions { 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 map) { + for (Include candidate : Include.values()) { + if (!this.includes.contains(candidate)) { + map.remove(candidate.key); + } + } + } + private EnumSet copyIncludes() { return (this.includes.isEmpty()) ? EnumSet.noneOf(Include.class) : EnumSet.copyOf(this.includes); } @@ -88,7 +102,7 @@ public final class ErrorAttributeOptions { * @return an {@code ErrorAttributeOptions} */ 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. */ - EXCEPTION, + EXCEPTION("exception"), /** * Include the stack trace attribute. */ - STACK_TRACE, + STACK_TRACE("trace"), /** * Include the message attribute. */ - MESSAGE, + MESSAGE("message"), /** * 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; + } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java index a618c45132e..ef6d8971ba5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributes.java @@ -70,18 +70,7 @@ public class DefaultErrorAttributes implements ErrorAttributes { @Override public Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { Map errorAttributes = getErrorAttributes(request, options.isIncluded(Include.STACK_TRACE)); - if (!options.isIncluded(Include.EXCEPTION)) { - 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"); - } + options.retainIncluded(errorAttributes); return errorAttributes; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java index 1dccd96f28d..3f89430b021 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributes.java @@ -91,18 +91,7 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException @Override public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { Map errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE)); - if (!options.isIncluded(Include.EXCEPTION)) { - 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"); - } + options.retainIncluded(errorAttributes); return errorAttributes; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java index 797dbcf8970..b3758250345 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/reactive/error/DefaultErrorAttributesTests.java @@ -106,7 +106,7 @@ class DefaultErrorAttributesTests { Exception error = new CustomException("Test Message"); MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); Map 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("message", "Test Message"); assertThat(attributes).containsEntry("status", HttpStatus.I_AM_A_TEAPOT.value()); @@ -117,7 +117,7 @@ class DefaultErrorAttributesTests { Exception error = new Custom2Exception(); MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); Map 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("status", HttpStatus.I_AM_A_TEAPOT.value()); assertThat(attributes).containsEntry("message", "Nope!"); @@ -176,7 +176,7 @@ class DefaultErrorAttributesTests { MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); ServerRequest serverRequest = buildServerRequest(request, error); Map 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("message", "invalid request"); assertThat(attributes).containsEntry("exception", RuntimeException.class.getName()); @@ -191,7 +191,7 @@ class DefaultErrorAttributesTests { MockServerHttpRequest request = MockServerHttpRequest.get("/test").build(); ServerRequest serverRequest = buildServerRequest(request, error); Map 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("message", "could not process request"); assertThat(attributes).containsEntry("exception", ResponseStatusException.class.getName()); @@ -283,6 +283,30 @@ class DefaultErrorAttributesTests { 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 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 attributes = this.errorAttributes.getErrorAttributes(serverRequest, + ErrorAttributeOptions.defaults().excluding(Include.ERROR)); + assertThat(attributes).doesNotContainKey("error"); + } + private ServerRequest buildServerRequest(MockServerHttpRequest request, Throwable error) { ServerWebExchange exchange = MockServerWebExchange.from(request); this.errorAttributes.storeErrorInformation(error, exchange); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java index 9ffa5cbffc1..5e4f8ba598d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/servlet/error/DefaultErrorAttributesTests.java @@ -294,4 +294,20 @@ class DefaultErrorAttributesTests { assertThat(attributes).containsEntry("message", "custom message"); } + @Test + void excludeStatus() { + this.request.setAttribute("jakarta.servlet.error.status_code", 404); + Map 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 attributes = this.errorAttributes.getErrorAttributes(this.webRequest, + ErrorAttributeOptions.defaults().excluding(Include.ERROR)); + assertThat(attributes).doesNotContainKey("error"); + } + }