Skip to content

Commit c00508d

Browse files
committed
Raise and handle NoResourceFoundException
See gh-29491
1 parent 83b0f4f commit c00508d

File tree

8 files changed

+195
-43
lines changed

8 files changed

+195
-43
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java

+24-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -50,6 +50,7 @@
5050
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
5151
import org.springframework.web.multipart.support.MissingServletRequestPartException;
5252
import org.springframework.web.servlet.NoHandlerFoundException;
53+
import org.springframework.web.servlet.resource.NoResourceFoundException;
5354
import org.springframework.web.util.WebUtils;
5455

5556
/**
@@ -121,6 +122,7 @@ protected MessageSource getMessageSource() {
121122
ServletRequestBindingException.class,
122123
MethodArgumentNotValidException.class,
123124
NoHandlerFoundException.class,
125+
NoResourceFoundException.class,
124126
AsyncRequestTimeoutException.class,
125127
ErrorResponseException.class,
126128
ConversionNotSupportedException.class,
@@ -158,6 +160,9 @@ else if (ex instanceof MethodArgumentNotValidException subEx) {
158160
else if (ex instanceof NoHandlerFoundException subEx) {
159161
return handleNoHandlerFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
160162
}
163+
else if (ex instanceof NoResourceFoundException subEx) {
164+
return handleNoResourceFoundException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
165+
}
161166
else if (ex instanceof AsyncRequestTimeoutException subEx) {
162167
return handleAsyncRequestTimeoutException(subEx, subEx.getHeaders(), subEx.getStatusCode(), request);
163168
}
@@ -348,6 +353,24 @@ protected ResponseEntity<Object> handleNoHandlerFoundException(
348353
return handleExceptionInternal(ex, null, headers, status, request);
349354
}
350355

356+
/**
357+
* Customize the handling of {@link NoResourceFoundException}.
358+
* <p>This method delegates to {@link #handleExceptionInternal}.
359+
* @param ex the exception to handle
360+
* @param headers the headers to use for the response
361+
* @param status the status code to use for the response
362+
* @param request the current request
363+
* @return a {@code ResponseEntity} for the response to use, possibly
364+
* {@code null} when the response is already committed
365+
* @since 6.1
366+
*/
367+
@Nullable
368+
protected ResponseEntity<Object> handleNoResourceFoundException(
369+
NoResourceFoundException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
370+
371+
return handleExceptionInternal(ex, null, headers, status, request);
372+
}
373+
351374
/**
352375
* Customize the handling of {@link AsyncRequestTimeoutException}.
353376
* <p>This method delegates to {@link #handleExceptionInternal}.

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java

+28
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.springframework.web.servlet.ModelAndView;
5151
import org.springframework.web.servlet.NoHandlerFoundException;
5252
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
53+
import org.springframework.web.servlet.resource.NoResourceFoundException;
5354
import org.springframework.web.util.WebUtils;
5455

5556
/**
@@ -125,6 +126,10 @@
125126
* <td><p>NoHandlerFoundException</p></td>
126127
* <td><p>404 (SC_NOT_FOUND)</p></td>
127128
* </tr>
129+
* <tr class="rowColor">
130+
* <td><p>NoResourceFoundException</p></td>
131+
* <td><p>404 (SC_NOT_FOUND)</p></td>
132+
* </tr>
128133
* <tr class="altColor">
129134
* <td><p>AsyncRequestTimeoutException</p></td>
130135
* <td><p>503 (SC_SERVICE_UNAVAILABLE)</p></td>
@@ -198,6 +203,9 @@ else if (ex instanceof MethodArgumentNotValidException theEx) {
198203
else if (ex instanceof NoHandlerFoundException theEx) {
199204
mav = handleNoHandlerFoundException(theEx, request, response, handler);
200205
}
206+
else if (ex instanceof NoResourceFoundException theEx) {
207+
mav = handleNoResourceFoundException(theEx, request, response, handler);
208+
}
201209
else if (ex instanceof AsyncRequestTimeoutException theEx) {
202210
mav = handleAsyncRequestTimeoutException(theEx, request, response, handler);
203211
}
@@ -413,6 +421,26 @@ protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
413421
return null;
414422
}
415423

424+
/**
425+
* Handle the case where no static resource was found.
426+
* <p>The default implementation returns {@code null} in which case the
427+
* exception is handled in {@link #handleErrorResponse}.
428+
* @param ex the {@link NoResourceFoundException} to be handled
429+
* @param request current HTTP request
430+
* @param response current HTTP response
431+
* @param handler the resource handler
432+
* @return an empty {@code ModelAndView} indicating the exception was handled, or
433+
* {@code null} indicating the exception should be handled in {@link #handleErrorResponse}
434+
* @throws IOException potentially thrown from {@link HttpServletResponse#sendError}
435+
* @since 6.1
436+
*/
437+
@Nullable
438+
protected ModelAndView handleNoResourceFoundException(NoResourceFoundException ex,
439+
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
440+
441+
return null;
442+
}
443+
416444
/**
417445
* Handle the case where an async request timed out.
418446
* <p>The default implementation returns {@code null} in which case the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.resource;
18+
19+
import jakarta.servlet.ServletException;
20+
21+
import org.springframework.http.HttpMethod;
22+
import org.springframework.http.HttpStatus;
23+
import org.springframework.http.HttpStatusCode;
24+
import org.springframework.http.ProblemDetail;
25+
import org.springframework.web.ErrorResponse;
26+
27+
/**
28+
* Raised when {@link ResourceHttpRequestHandler} can not find a resource.
29+
*
30+
* @author Rossen Stoyanchev
31+
* @since 6.1
32+
* @see org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
33+
* @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
34+
*/
35+
@SuppressWarnings("serial")
36+
public class NoResourceFoundException extends ServletException implements ErrorResponse {
37+
38+
private final HttpMethod httpMethod;
39+
40+
private final String resourcePath;
41+
42+
private final ProblemDetail body;
43+
44+
45+
/**
46+
* Create an instance.
47+
*/
48+
public NoResourceFoundException(HttpMethod httpMethod, String resourcePath) {
49+
super("No static resource " + resourcePath + ".");
50+
this.httpMethod = httpMethod;
51+
this.resourcePath = resourcePath;
52+
this.body = ProblemDetail.forStatusAndDetail(getStatusCode(), getMessage());
53+
}
54+
55+
56+
/**
57+
* Return the HTTP method for the request.
58+
*/
59+
public HttpMethod getHttpMethod() {
60+
return this.httpMethod;
61+
}
62+
63+
/**
64+
* Return the path used to locate the resource.
65+
* @see org.springframework.web.servlet.HandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
66+
*/
67+
public String getResourcePath() {
68+
return this.resourcePath;
69+
}
70+
71+
@Override
72+
public HttpStatusCode getStatusCode() {
73+
return HttpStatus.NOT_FOUND;
74+
}
75+
76+
@Override
77+
public ProblemDetail getBody() {
78+
return this.body;
79+
}
80+
81+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java

+14-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -537,8 +537,8 @@ protected org.springframework.web.accept.PathExtensionContentNegotiationStrategy
537537

538538
/**
539539
* Processes a resource request.
540-
* <p>Checks for the existence of the requested resource in the configured list of locations.
541-
* If the resource does not exist, a {@code 404} response will be returned to the client.
540+
* <p>Finds the requested resource under one of the configured locations.
541+
* If the resource does not exist, {@link NoResourceFoundException} is raised.
542542
* If the resource exists, the request will be checked for the presence of the
543543
* {@code Last-Modified} header, and its value will be compared against the last-modified
544544
* timestamp of the given resource, returning a {@code 304} status code if the
@@ -555,8 +555,7 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon
555555
Resource resource = getResource(request);
556556
if (resource == null) {
557557
logger.debug("Resource not found");
558-
response.sendError(HttpServletResponse.SC_NOT_FOUND);
559-
return;
558+
throw new NoResourceFoundException(HttpMethod.valueOf(request.getMethod()), getPath(request));
560559
}
561560

562561
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
@@ -611,12 +610,7 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon
611610

612611
@Nullable
613612
protected Resource getResource(HttpServletRequest request) throws IOException {
614-
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
615-
if (path == null) {
616-
throw new IllegalStateException("Required request attribute '" +
617-
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
618-
}
619-
613+
String path = getPath(request);
620614
path = processPath(path);
621615
if (!StringUtils.hasText(path) || isInvalidPath(path)) {
622616
return null;
@@ -635,6 +629,15 @@ protected Resource getResource(HttpServletRequest request) throws IOException {
635629
return resource;
636630
}
637631

632+
private static String getPath(HttpServletRequest request) {
633+
String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
634+
if (path == null) {
635+
throw new IllegalStateException("Required request attribute '" +
636+
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
637+
}
638+
return path;
639+
}
640+
638641
/**
639642
* Process the given resource path.
640643
* <p>The default implementation replaces:

spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
import org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler;
115115
import org.springframework.web.servlet.resource.EncodedResourceResolver;
116116
import org.springframework.web.servlet.resource.FixedVersionStrategy;
117+
import org.springframework.web.servlet.resource.NoResourceFoundException;
117118
import org.springframework.web.servlet.resource.PathResourceResolver;
118119
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
119120
import org.springframework.web.servlet.resource.ResourceResolver;
@@ -147,6 +148,7 @@
147148

148149
import static org.assertj.core.api.Assertions.assertThat;
149150
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
151+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
150152
import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN;
151153

152154
/**
@@ -417,8 +419,8 @@ public void testResources() throws Exception {
417419
for (HandlerInterceptor interceptor : chain.getInterceptorList()) {
418420
interceptor.preHandle(request, response, chain.getHandler());
419421
}
420-
ModelAndView mv = adapter.handle(request, response, chain.getHandler());
421-
assertThat((Object) mv).isNull();
422+
assertThatThrownBy(() -> adapter.handle(request, response, chain.getHandler()))
423+
.isInstanceOf(NoResourceFoundException.class);
422424
}
423425

424426
@Test

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java

+6
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
import org.springframework.web.servlet.ModelAndView;
6464
import org.springframework.web.servlet.NoHandlerFoundException;
6565
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
66+
import org.springframework.web.servlet.resource.NoResourceFoundException;
6667
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
6768
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
6869
import org.springframework.web.testfixture.servlet.MockServletConfig;
@@ -265,6 +266,11 @@ public void noHandlerFoundException() {
265266
assertThat(responseEntity.getHeaders()).isEmpty();
266267
}
267268

269+
@Test
270+
public void noResourceFoundException() {
271+
testException(new NoResourceFoundException(HttpMethod.GET, "/resource"));
272+
}
273+
268274
@Test
269275
public void asyncRequestTimeoutException() {
270276
testException(new AsyncRequestTimeoutException());

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolverTests.java

+15-5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.web.servlet.HandlerExceptionResolver;
4949
import org.springframework.web.servlet.ModelAndView;
5050
import org.springframework.web.servlet.NoHandlerFoundException;
51+
import org.springframework.web.servlet.resource.NoResourceFoundException;
5152
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
5253
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
5354

@@ -182,7 +183,7 @@ public void handleMethodArgumentNotValid() throws Exception {
182183
}
183184

184185
@Test
185-
public void handleMissingServletRequestPartException() throws Exception {
186+
public void handleMissingServletRequestPartException() {
186187
MissingServletRequestPartException ex = new MissingServletRequestPartException("name");
187188
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
188189
assertThat(mav).as("No ModelAndView returned").isNotNull();
@@ -194,7 +195,7 @@ public void handleMissingServletRequestPartException() throws Exception {
194195
}
195196

196197
@Test
197-
public void handleBindException() throws Exception {
198+
public void handleBindException() {
198199
BindException ex = new BindException(new Object(), "name");
199200
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
200201
assertThat(mav).as("No ModelAndView returned").isNotNull();
@@ -203,7 +204,7 @@ public void handleBindException() throws Exception {
203204
}
204205

205206
@Test
206-
public void handleNoHandlerFoundException() throws Exception {
207+
public void handleNoHandlerFoundException() {
207208
ServletServerHttpRequest req = new ServletServerHttpRequest(
208209
new MockHttpServletRequest("GET","/resource"));
209210
NoHandlerFoundException ex = new NoHandlerFoundException(req.getMethod().name(),
@@ -215,7 +216,16 @@ public void handleNoHandlerFoundException() throws Exception {
215216
}
216217

217218
@Test
218-
public void handleConversionNotSupportedException() throws Exception {
219+
public void handleNoResourceFoundException() {
220+
NoResourceFoundException ex = new NoResourceFoundException(HttpMethod.GET, "/resource");
221+
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
222+
assertThat(mav).as("No ModelAndView returned").isNotNull();
223+
assertThat(mav.isEmpty()).as("No Empty ModelAndView returned").isTrue();
224+
assertThat(response.getStatus()).as("Invalid status code").isEqualTo(404);
225+
}
226+
227+
@Test
228+
public void handleConversionNotSupportedException() {
219229
ConversionNotSupportedException ex =
220230
new ConversionNotSupportedException(new Object(), String.class, new Exception());
221231
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
@@ -228,7 +238,7 @@ public void handleConversionNotSupportedException() throws Exception {
228238
}
229239

230240
@Test // SPR-14669
231-
public void handleAsyncRequestTimeoutException() throws Exception {
241+
public void handleAsyncRequestTimeoutException() {
232242
Exception ex = new AsyncRequestTimeoutException();
233243
ModelAndView mav = exceptionResolver.resolveException(request, response, null, ex);
234244
assertThat(mav).as("No ModelAndView returned").isNotNull();

0 commit comments

Comments
 (0)