Raise and handle NoResourceFoundException

See gh-29491
This commit is contained in:
rstoyanchev 2023-06-19 16:22:34 +01:00
parent 83b0f4f394
commit c00508d6cf
8 changed files with 195 additions and 43 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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 org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import org.springframework.web.util.WebUtils;
/**
@ -121,6 +122,7 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
ServletRequestBindingException.class,
MethodArgumentNotValidException.class,
NoHandlerFoundException.class,
NoResourceFoundException.class,
AsyncRequestTimeoutException.class,
ErrorResponseException.class,
ConversionNotSupportedException.class,
@ -158,6 +160,9 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
else if (ex instanceof NoHandlerFoundException subEx) {
return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
}
else if (ex instanceof NoResourceFoundException subEx) {
return handleNoResourceFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
}
else if (ex instanceof AsyncRequestTimeoutException subEx) {
return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
}
@ -348,6 +353,24 @@ public abstract class ResponseEntityExceptionHandler implements MessageSourceAwa
return handleExceptionInternal(ex, null, headers, status, request);
}
/**
* Customize the handling of {@link NoResourceFoundException}.
* <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception to handle
* @param headers the headers to use for the response
* @param status the status code to use for the response
* @param request the current request
* @return a {@code ResponseEntity} for the response to use, possibly
* {@code null} when the response is already committed
* @since 6.1
*/
@Nullable
protected ResponseEntity<Object> handleNoResourceFoundException(
NoResourceFoundException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request);
}
/**
* Customize the handling of {@link AsyncRequestTimeoutException}.
* <p>This method delegates to {@link #handleExceptionInternal}.

View File

@ -50,6 +50,7 @@ import org.springframework.web.multipart.support.MissingServletRequestPartExcept
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import org.springframework.web.util.WebUtils;
/**
@ -125,6 +126,10 @@ import org.springframework.web.util.WebUtils;
* <td><p>NoHandlerFoundException</p></td>
* <td><p>404 (SC_NOT_FOUND)</p></td>
* </tr>
* <tr class="rowColor">
* <td><p>NoResourceFoundException</p></td>
* <td><p>404 (SC_NOT_FOUND)</p></td>
* </tr>
* <tr class="altColor">
* <td><p>AsyncRequestTimeoutException</p></td>
* <td><p>503 (SC_SERVICE_UNAVAILABLE)</p></td>
@ -198,6 +203,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
else if (ex instanceof NoHandlerFoundException theEx) {
mav = handleNoHandlerFoundException(theEx, request, response, handler);
}
else if (ex instanceof NoResourceFoundException theEx) {
mav = handleNoResourceFoundException(theEx, request, response, handler);
}
else if (ex instanceof AsyncRequestTimeoutException theEx) {
mav = handleAsyncRequestTimeoutException(theEx, request, response, handler);
}
@ -413,6 +421,26 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
return null;
}
/**
* Handle the case where no static resource was found.
* <p>The default implementation returns {@code null} in which case the
* exception is handled in {@link #handleErrorResponse}.
* @param ex the {@link NoResourceFoundException} to be handled
* @param request current HTTP request
* @param response current HTTP response
* @param handler the resource handler
* @return an empty {@code ModelAndView} indicating the exception was handled, or
* {@code null} indicating the exception should be handled in {@link #handleErrorResponse}
* @throws IOException potentially thrown from {@link HttpServletResponse#sendError}
* @since 6.1
*/
@Nullable
protected ModelAndView handleNoResourceFoundException(NoResourceFoundException ex,
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
return null;
}
/**
* Handle the case where an async request timed out.
* <p>The default implementation returns {@code null} in which case the

View File

@ -0,0 +1,81 @@
/*
* Copyright 2002-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.resource;
import jakarta.servlet.ServletException;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.web.ErrorResponse;
/**
* Raised when {@link ResourceHttpRequestHandler} can not find a resource.
*
* @author Rossen Stoyanchev
* @since 6.1
* @see org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
* @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
*/
@SuppressWarnings("serial")
public class NoResourceFoundException extends ServletException implements ErrorResponse {
private final HttpMethod httpMethod;
private final String resourcePath;
private final ProblemDetail body;
/**
* Create an instance.
*/
public NoResourceFoundException(HttpMethod httpMethod, String resourcePath) {
super("No static resource " + resourcePath + ".");
this.httpMethod = httpMethod;
this.resourcePath = resourcePath;
this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), getMessage());
}
/**
* Return the HTTP method for the request.
*/
public HttpMethod getHttpMethod() {
return this.httpMethod;
}
/**
* Return the path used to locate the resource.
* @see org.springframework.web.servlet.HandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
*/
public String getResourcePath() {
return this.resourcePath;
}
@Override
public HttpStatusCode getStatusCode() {
return HttpStatus.NOT_FOUND;
}
@Override
public ProblemDetail getBody() {
return this.body;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -537,8 +537,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/**
* Processes a resource request.
* <p>Checks for the existence of the requested resource in the configured list of locations.
* If the resource does not exist, a {@code 404} response will be returned to the client.
* <p>Finds the requested resource under one of the configured locations.
* If the resource does not exist, {@link NoResourceFoundException} is raised.
* If the resource exists, the request will be checked for the presence of the
* {@code Last-Modified} header, and its value will be compared against the last-modified
* timestamp of the given resource, returning a {@code 304} status code if the
@ -555,8 +555,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
Resource resource = getResource(request);
if (resource == null) {
logger.debug("Resource not found");
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
throw new NoResourceFoundException(HttpMethod.valueOf(request.getMethod()), getPath(request));
}
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
@ -611,12 +610,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
@Nullable
protected Resource getResource(HttpServletRequest request) throws IOException {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if (path == null) {
throw new IllegalStateException("Required request attribute '" +
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
}
String path = getPath(request);
path = processPath(path);
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
return null;
@ -635,6 +629,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return resource;
}
private static String getPath(HttpServletRequest request) {
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if (path == null) {
throw new IllegalStateException("Required request attribute '" +
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
}
return path;
}
/**
* Process the given resource path.
* <p>The default implementation replaces:

View File

@ -114,6 +114,7 @@ import org.springframework.web.servlet.resource.CssLinkResourceTransformer;
import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler;
import org.springframework.web.servlet.resource.EncodedResourceResolver;
import org.springframework.web.servlet.resource.FixedVersionStrategy;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.servlet.resource.ResourceResolver;
@ -147,6 +148,7 @@ import org.springframework.web.util.UrlPathHelper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
/**
@ -417,8 +419,8 @@ public class MvcNamespaceTests {
for (HandlerInterceptor interceptor : chain.getInterceptorList()) {
interceptor.preHandle(request, response, chain.getHandler());
}
ModelAndView mv = adapter.handle(request, response, chain.getHandler());
assertThat((Object) mv).isNull();
assertThatThrownBy(() -> adapter.handle(request, response, chain.getHandler()))
.isInstanceOf(NoResourceFoundException.class);
}
@Test

View File

@ -63,6 +63,7 @@ import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.testfixture.servlet.MockServletConfig;
@ -265,6 +266,11 @@ public class ResponseEntityExceptionHandlerTests {
assertThat(responseEntity.getHeaders()).isEmpty();
}
@Test
public void noResourceFoundException() {
testException(new NoResourceFoundException(HttpMethod.GET, "/resource"));
}
@Test
public void asyncRequestTimeoutException() {
testException(new AsyncRequestTimeoutException());

View File

@ -48,6 +48,7 @@ import org.springframework.web.multipart.support.MissingServletRequestPartExcept
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
@ -182,7 +183,7 @@ public class DefaultHandlerExceptionResolverTests {
}
@Test
public void handleMissingServletRequestPartException() throws Exception {
public void handleMissingServletRequestPartException() {
MissingServletRequestPartException ex = new MissingServletRequestPartException("name");
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertThat(mav).as("No ModelAndView returned").isNotNull();
@ -194,7 +195,7 @@ public class DefaultHandlerExceptionResolverTests {
}
@Test
public void handleBindException() throws Exception {
public void handleBindException() {
BindException ex = new BindException(new Object(), "name");
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertThat(mav).as("No ModelAndView returned").isNotNull();
@ -203,7 +204,7 @@ public class DefaultHandlerExceptionResolverTests {
}
@Test
public void handleNoHandlerFoundException() throws Exception {
public void handleNoHandlerFoundException() {
ServletServerHttpRequest req = new ServletServerHttpRequest(
new MockHttpServletRequest("GET","/resource"));
NoHandlerFoundException ex = new NoHandlerFoundException(req.getMethod().name(),
@ -215,7 +216,16 @@ public class DefaultHandlerExceptionResolverTests {
}
@Test
public void handleConversionNotSupportedException() throws Exception {
public void handleNoResourceFoundException() {
NoResourceFoundException ex = new NoResourceFoundException(HttpMethod.GET, "/resource");
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertThat(mav).as("No ModelAndView returned").isNotNull();
assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue();
assertThat(response.getStatus()).as("Invalid status code").isEqualTo(404);
}
@Test
public void handleConversionNotSupportedException() {
ConversionNotSupportedException ex =
new ConversionNotSupportedException(new Object(), String.class, new Exception());
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
@ -228,7 +238,7 @@ public class DefaultHandlerExceptionResolverTests {
}
@Test // SPR-14669
public void handleAsyncRequestTimeoutException() throws Exception {
public void handleAsyncRequestTimeoutException() {
Exception ex = new AsyncRequestTimeoutException();
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
assertThat(mav).as("No ModelAndView returned").isNotNull();

View File

@ -29,7 +29,6 @@ import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@ -44,6 +43,7 @@ import org.springframework.web.testfixture.servlet.MockServletContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
@ -367,11 +367,11 @@ class ResourceHttpRequestHandlerTests {
testInvalidPath("%2F%2F%2E%2E%2F%2F%2E%2E" + secretPath, handler);
}
private void testInvalidPath(String requestPath, ResourceHttpRequestHandler handler) throws Exception {
private void testInvalidPath(String requestPath, ResourceHttpRequestHandler handler) {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath);
this.response = new MockHttpServletResponse();
handler.handleRequest(this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertThatThrownBy(() -> handler.handleRequest(this.request, this.response))
.isInstanceOf(NoResourceFoundException.class);
}
@Test
@ -409,19 +409,17 @@ class ResourceHttpRequestHandlerTests {
testResolvePathWithTraversal(location, "/ " + secretPath);
}
private void testResolvePathWithTraversal(Resource location, String requestPath) throws Exception {
private void testResolvePathWithTraversal(Resource location, String requestPath) {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, requestPath);
this.response = new MockHttpServletResponse();
this.handler.handleRequest(this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertNotFound();
}
@Test
void ignoreInvalidEscapeSequence() throws Exception {
void ignoreInvalidEscapeSequence() {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/%foo%/bar.txt");
this.response = new MockHttpServletResponse();
this.handler.handleRequest(this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(404);
assertNotFound();
}
@Test
@ -506,32 +504,29 @@ class ResourceHttpRequestHandlerTests {
@Test
void directory() throws Exception {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "js/");
this.handler.handleRequest(this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(404);
assertNotFound();
}
@Test
void directoryInJarFile() throws Exception {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "underscorejs/");
this.handler.handleRequest(this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(404);
assertNotFound();
}
@Test
void missingResourcePath() throws Exception {
void missingResourcePath() {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "");
this.handler.handleRequest(this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(404);
assertNotFound();
}
@Test
void noPathWithinHandlerMappingAttribute() throws Exception {
void noPathWithinHandlerMappingAttribute() {
assertThatIllegalStateException().isThrownBy(() ->
this.handler.handleRequest(this.request, this.response));
}
@Test
void unsupportedHttpMethod() throws Exception {
void unsupportedHttpMethod() {
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
this.request.setMethod("POST");
assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class).isThrownBy(() ->
@ -539,19 +534,18 @@ class ResourceHttpRequestHandlerTests {
}
@Test
void resourceNotFound() throws Exception {
void testResourceNotFound() {
for (HttpMethod method : HttpMethod.values()) {
this.request = new MockHttpServletRequest("GET", "");
this.response = new MockHttpServletResponse();
resourceNotFound(method);
testResourceNotFound(method);
}
}
private void resourceNotFound(HttpMethod httpMethod) throws Exception {
private void testResourceNotFound(HttpMethod httpMethod) {
this.request.setMethod(httpMethod.name());
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "not-there.css");
this.handler.handleRequest(this.request, this.response);
assertThat(this.response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value());
assertNotFound();
}
@Test
@ -756,6 +750,11 @@ class ResourceHttpRequestHandlerTests {
}
private void assertNotFound() {
assertThatThrownBy(() -> this.handler.handleRequest(this.request, this.response))
.isInstanceOf(NoResourceFoundException.class);
}
private long resourceLastModified(String resourceName) throws IOException {
return new ClassPathResource(resourceName, getClass()).getFile().lastModified();
}