Add property to control 'path' field inclusion in error responses

By default it is included.

Closes gh-38619
This commit is contained in:
Moritz Halbritter 2023-12-05 08:37:13 +01:00
parent c4be302fdb
commit 2cce123bb5
12 changed files with 157 additions and 15 deletions

View File

@ -36,6 +36,7 @@ import org.springframework.web.context.request.ServletWebRequest;
*
* @author Dave Syer
* @author Scott Frederick
* @author Moritz Halbritter
* @since 2.0.0
*/
@Controller
@ -72,6 +73,7 @@ public class ManagementErrorEndpoint {
if (includeBindingErrors(request)) {
options = options.including(Include.BINDING_ERRORS);
}
options = includePath(request) ? options.including(Include.PATH) : options.excluding(Include.PATH);
return options;
}
@ -79,7 +81,7 @@ public class ManagementErrorEndpoint {
return switch (this.errorProperties.getIncludeStacktrace()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "trace");
default -> false;
case NEVER -> false;
};
}
@ -87,7 +89,7 @@ public class ManagementErrorEndpoint {
return switch (this.errorProperties.getIncludeMessage()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "message");
default -> false;
case NEVER -> false;
};
}
@ -95,7 +97,15 @@ public class ManagementErrorEndpoint {
return switch (this.errorProperties.getIncludeBindingErrors()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "errors");
default -> false;
case NEVER -> false;
};
}
private boolean includePath(ServletWebRequest request) {
return switch (this.errorProperties.getIncludePath()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "path");
case NEVER -> false;
};
}

View File

@ -55,6 +55,11 @@ public class ErrorProperties {
*/
private IncludeAttribute includeBindingErrors = IncludeAttribute.NEVER;
/**
* When to include "path" attribute.
*/
private IncludeAttribute includePath = IncludeAttribute.ALWAYS;
private final Whitelabel whitelabel = new Whitelabel();
public String getPath() {
@ -97,6 +102,14 @@ public class ErrorProperties {
this.includeBindingErrors = includeBindingErrors;
}
public IncludeAttribute getIncludePath() {
return this.includePath;
}
public void setIncludePath(IncludeAttribute includePath) {
this.includePath = includePath;
}
public Whitelabel getWhitelabel() {
return this.whitelabel;
}

View File

@ -54,6 +54,7 @@ import org.springframework.web.util.HtmlUtils;
*
* @author Brian Clozel
* @author Scott Frederick
* @author Moritz Halbritter
* @since 2.0.0
* @see ErrorAttributes
*/
@ -168,6 +169,17 @@ public abstract class AbstractErrorWebExceptionHandler implements ErrorWebExcept
return getBooleanParameter(request, "errors");
}
/**
* Check whether the path attribute has been set on the given request.
* @param request the source request
* @return {@code true} if the path attribute has been requested, {@code false}
* otherwise
* @since 3.3.0
*/
protected boolean isPathEnabled(ServerRequest request) {
return getBooleanParameter(request, "path");
}
private boolean getBooleanParameter(ServerRequest request, String parameterName) {
String parameter = request.queryParam(parameterName).orElse("false");
return !"false".equalsIgnoreCase(parameter);

View File

@ -75,6 +75,7 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
*
* @author Brian Clozel
* @author Scott Frederick
* @author Moritz Halbritter
* @since 2.0.0
*/
public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
@ -164,6 +165,7 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(Include.BINDING_ERRORS);
}
options = isIncludePath(request, mediaType) ? options.including(Include.PATH) : options.excluding(Include.PATH);
return options;
}
@ -177,7 +179,7 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
return switch (this.errorProperties.getIncludeStacktrace()) {
case ALWAYS -> true;
case ON_PARAM -> isTraceEnabled(request);
default -> false;
case NEVER -> false;
};
}
@ -191,7 +193,7 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
return switch (this.errorProperties.getIncludeMessage()) {
case ALWAYS -> true;
case ON_PARAM -> isMessageEnabled(request);
default -> false;
case NEVER -> false;
};
}
@ -205,7 +207,22 @@ public class DefaultErrorWebExceptionHandler extends AbstractErrorWebExceptionHa
return switch (this.errorProperties.getIncludeBindingErrors()) {
case ALWAYS -> true;
case ON_PARAM -> isBindingErrorsEnabled(request);
default -> false;
case NEVER -> false;
};
}
/**
* Determine if the path attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the path attribute should be included
* @since 3.3.0
*/
protected boolean isIncludePath(ServerRequest request, MediaType produces) {
return switch (this.errorProperties.getIncludePath()) {
case ALWAYS -> true;
case ON_PARAM -> isPathEnabled(request);
case NEVER -> false;
};
}

View File

@ -41,6 +41,7 @@ import org.springframework.web.servlet.ModelAndView;
* @author Dave Syer
* @author Phillip Webb
* @author Scott Frederick
* @author Moritz Halbritter
* @since 1.3.0
* @see ErrorAttributes
*/
@ -74,18 +75,43 @@ public abstract class AbstractErrorController implements ErrorController {
return this.errorAttributes.getErrorAttributes(webRequest, options);
}
/**
* Returns whether the trace parameter is set.
* @param request the request
* @return whether the trace parameter is set
*/
protected boolean getTraceParameter(HttpServletRequest request) {
return getBooleanParameter(request, "trace");
}
/**
* Returns whether the message parameter is set.
* @param request the request
* @return whether the message parameter is set
*/
protected boolean getMessageParameter(HttpServletRequest request) {
return getBooleanParameter(request, "message");
}
/**
* Returns whether the errors parameter is set.
* @param request the request
* @return whether the errors parameter is set
*/
protected boolean getErrorsParameter(HttpServletRequest request) {
return getBooleanParameter(request, "errors");
}
/**
* Returns whether the path parameter is set.
* @param request the request
* @return whether the path parameter is set
* @since 3.3.0
*/
protected boolean getPathParameter(HttpServletRequest request) {
return getBooleanParameter(request, "path");
}
protected boolean getBooleanParameter(HttpServletRequest request, String parameterName) {
String parameter = request.getParameter(parameterName);
if (parameter == null) {

View File

@ -49,6 +49,7 @@ import org.springframework.web.servlet.ModelAndView;
* @author Michael Stummvoll
* @author Stephane Nicoll
* @author Scott Frederick
* @author Moritz Halbritter
* @since 1.0.0
* @see ErrorAttributes
* @see ErrorProperties
@ -121,6 +122,7 @@ public class BasicErrorController extends AbstractErrorController {
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(Include.BINDING_ERRORS);
}
options = isIncludePath(request, mediaType) ? options.including(Include.PATH) : options.excluding(Include.PATH);
return options;
}
@ -134,7 +136,7 @@ public class BasicErrorController extends AbstractErrorController {
return switch (getErrorProperties().getIncludeStacktrace()) {
case ALWAYS -> true;
case ON_PARAM -> getTraceParameter(request);
default -> false;
case NEVER -> false;
};
}
@ -148,7 +150,7 @@ public class BasicErrorController extends AbstractErrorController {
return switch (getErrorProperties().getIncludeMessage()) {
case ALWAYS -> true;
case ON_PARAM -> getMessageParameter(request);
default -> false;
case NEVER -> false;
};
}
@ -162,7 +164,22 @@ public class BasicErrorController extends AbstractErrorController {
return switch (getErrorProperties().getIncludeBindingErrors()) {
case ALWAYS -> true;
case ON_PARAM -> getErrorsParameter(request);
default -> false;
case NEVER -> false;
};
}
/**
* Determine if the path attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the path attribute should be included
* @since 3.3.0
*/
protected boolean isIncludePath(HttpServletRequest request, MediaType produces) {
return switch (getErrorProperties().getIncludePath()) {
case ALWAYS -> true;
case ON_PARAM -> getPathParameter(request);
case NEVER -> false;
};
}

View File

@ -112,8 +112,7 @@ class ErrorMvcAutoConfigurationTests {
}
private ErrorAttributeOptions withAllOptions() {
return ErrorAttributeOptions.of(Include.EXCEPTION, Include.STACK_TRACE, Include.MESSAGE,
Include.BINDING_ERRORS);
return ErrorAttributeOptions.of(Include.values());
}
}

View File

@ -88,7 +88,7 @@ public final class ErrorAttributeOptions {
* @return an {@code ErrorAttributeOptions}
*/
public static ErrorAttributeOptions defaults() {
return of();
return of(Include.PATH);
}
/**
@ -135,7 +135,13 @@ public final class ErrorAttributeOptions {
/**
* Include the binding errors attribute.
*/
BINDING_ERRORS
BINDING_ERRORS,
/**
* Include the request path.
* @since 3.3.0
*/
PATH
}

View File

@ -57,6 +57,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Stephane Nicoll
* @author Michele Mancioppi
* @author Scott Frederick
* @author Moritz Halbritter
* @since 2.0.0
* @see ErrorAttributes
*/
@ -79,6 +80,9 @@ public class DefaultErrorAttributes implements ErrorAttributes {
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
if (!options.isIncluded(Include.PATH)) {
errorAttributes.remove("path");
}
return errorAttributes;
}

View File

@ -61,6 +61,7 @@ import org.springframework.web.servlet.ModelAndView;
* @author Stephane Nicoll
* @author Vedran Pavic
* @author Scott Frederick
* @author Moritz Halbritter
* @since 2.0.0
* @see ErrorAttributes
*/
@ -100,6 +101,9 @@ public class DefaultErrorAttributes implements ErrorAttributes, HandlerException
if (!options.isIncluded(Include.BINDING_ERRORS)) {
errorAttributes.remove("errors");
}
if (!options.isIncluded(Include.PATH)) {
errorAttributes.remove("path");
}
return errorAttributes;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -50,6 +50,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* @author Brian Clozel
* @author Stephane Nicoll
* @author Scott Frederick
* @author Moritz Halbritter
*/
class DefaultErrorAttributesTests {
@ -212,6 +213,14 @@ class DefaultErrorAttributesTests {
assertThat(attributes.get("trace").toString()).startsWith("java.lang");
}
@Test
void includePathByDefault() {
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, NOT_FOUND),
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("path", "/test");
}
@Test
void includePath() {
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
@ -220,6 +229,14 @@ class DefaultErrorAttributesTests {
assertThat(attributes).containsEntry("path", "/test");
}
@Test
void excludePath() {
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(buildServerRequest(request, NOT_FOUND),
ErrorAttributeOptions.of());
assertThat(attributes).doesNotContainEntry("path", "/test");
}
@Test
void includeLogPrefix() {
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();

View File

@ -47,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Phillip Webb
* @author Vedran Pavic
* @author Scott Frederick
* @author Moritz Halbritter
*/
class DefaultErrorAttributesTests {
@ -249,13 +250,29 @@ class DefaultErrorAttributesTests {
}
@Test
void path() {
void shouldIncludePathByDefault() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("path", "path");
}
@Test
void shouldIncludePath() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.PATH));
assertThat(attributes).containsEntry("path", "path");
}
@Test
void shouldExcludePath() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of());
assertThat(attributes).doesNotContainEntry("path", "path");
}
@Test
void whenGetMessageIsOverriddenThenMessageAttributeContainsValueReturnedFromIt() {
Map<String, Object> attributes = new DefaultErrorAttributes() {