Skip to content

Commit 78d0dbb

Browse files
committed
Ensure handling of 404 errors for static resources
Closes gh-30930
1 parent 85704c8 commit 78d0dbb

File tree

6 files changed

+194
-10
lines changed

6 files changed

+194
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.reactive.resource;
18+
19+
import org.springframework.http.HttpStatus;
20+
import org.springframework.web.server.ResponseStatusException;
21+
22+
/**
23+
* Raised when {@link ResourceWebHandler} is mapped to the request but can not
24+
* find a matching resource.
25+
*
26+
* @author Rossen Stoyanchev
27+
* @since 6.1
28+
*/
29+
@SuppressWarnings("serial")
30+
public class NoResourceFoundException extends ResponseStatusException {
31+
32+
33+
public NoResourceFoundException(String resourcePath) {
34+
super(HttpStatus.NOT_FOUND, "No static resource " + resourcePath + ".");
35+
setDetail(getReason());
36+
}
37+
38+
}

spring-webflux/src/main/java/org/springframework/web/reactive/resource/ResourceWebHandler.java

+1-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import org.springframework.http.CacheControl;
4242
import org.springframework.http.HttpHeaders;
4343
import org.springframework.http.HttpMethod;
44-
import org.springframework.http.HttpStatus;
4544
import org.springframework.http.MediaType;
4645
import org.springframework.http.MediaTypeFactory;
4746
import org.springframework.http.codec.ResourceHttpMessageWriter;
@@ -54,7 +53,6 @@
5453
import org.springframework.util.StringUtils;
5554
import org.springframework.web.reactive.HandlerMapping;
5655
import org.springframework.web.server.MethodNotAllowedException;
57-
import org.springframework.web.server.ResponseStatusException;
5856
import org.springframework.web.server.ServerWebExchange;
5957
import org.springframework.web.server.WebHandler;
6058
import org.springframework.web.util.pattern.PathPattern;
@@ -403,7 +401,7 @@ public Mono<Void> handle(ServerWebExchange exchange) {
403401
return getResource(exchange)
404402
.switchIfEmpty(Mono.defer(() -> {
405403
logger.debug(exchange.getLogPrefix() + "Resource not found");
406-
return Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND));
404+
return Mono.error(new NoResourceFoundException(getResourcePath(exchange)));
407405
}))
408406
.flatMap(resource -> {
409407
try {

spring-webflux/src/test/java/org/springframework/web/reactive/DispatcherHandlerErrorTests.java

+82-5
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.
@@ -19,6 +19,7 @@
1919
import java.time.Duration;
2020
import java.util.Collections;
2121
import java.util.List;
22+
import java.util.Map;
2223
import java.util.concurrent.atomic.AtomicReference;
2324

2425
import org.junit.jupiter.api.BeforeEach;
@@ -32,22 +33,31 @@
3233
import org.springframework.context.annotation.Configuration;
3334
import org.springframework.core.codec.CharSequenceEncoder;
3435
import org.springframework.http.HttpStatus;
36+
import org.springframework.http.MediaType;
3537
import org.springframework.http.codec.EncoderHttpMessageWriter;
38+
import org.springframework.http.codec.ServerCodecConfigurer;
3639
import org.springframework.stereotype.Controller;
40+
import org.springframework.web.bind.annotation.ControllerAdvice;
3741
import org.springframework.web.bind.annotation.RequestBody;
3842
import org.springframework.web.bind.annotation.RequestMapping;
3943
import org.springframework.web.bind.annotation.ResponseBody;
4044
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
45+
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
46+
import org.springframework.web.reactive.resource.ResourceWebHandler;
47+
import org.springframework.web.reactive.result.SimpleHandlerAdapter;
4148
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
4249
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
4350
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
51+
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
52+
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;
4453
import org.springframework.web.server.NotAcceptableStatusException;
4554
import org.springframework.web.server.ResponseStatusException;
4655
import org.springframework.web.server.ServerWebExchange;
4756
import org.springframework.web.server.WebExceptionHandler;
4857
import org.springframework.web.server.WebHandler;
4958
import org.springframework.web.server.handler.ExceptionHandlingWebHandler;
5059
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
60+
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
5161
import org.springframework.web.testfixture.server.MockServerWebExchange;
5262

5363
import static org.assertj.core.api.Assertions.assertThat;
@@ -69,10 +79,11 @@ public class DispatcherHandlerErrorTests {
6979

7080
@BeforeEach
7181
public void setup() {
72-
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
73-
ctx.register(TestConfig.class);
74-
ctx.refresh();
75-
this.dispatcherHandler = new DispatcherHandler(ctx);
82+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
83+
context.register(TestConfig.class);
84+
context.refresh();
85+
86+
this.dispatcherHandler = new DispatcherHandler(context);
7687
}
7788

7889

@@ -94,6 +105,28 @@ public void noHandler() {
94105
StepVerifier.create(mono).consumeErrorWith(ex -> assertThat(ex).isNotSameAs(exceptionRef.get())).verify();
95106
}
96107

108+
@Test
109+
public void noStaticResource() {
110+
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
111+
context.register(StaticResourceConfig.class);
112+
context.refresh();
113+
114+
MockServerHttpRequest request = MockServerHttpRequest.get("/resources/non-existing").build();
115+
MockServerWebExchange exchange = MockServerWebExchange.from(request);
116+
new DispatcherHandler(context).handle(exchange).block();
117+
118+
MockServerHttpResponse response = exchange.getResponse();
119+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
120+
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON);
121+
assertThat(response.getBodyAsString().block()).isEqualTo("""
122+
{"type":"about:blank",\
123+
"title":"Not Found",\
124+
"status":404,\
125+
"detail":"No static resource non-existing.",\
126+
"instance":"/resources/non-existing"}\
127+
""");
128+
}
129+
97130
@Test
98131
public void controllerReturnsMonoError() {
99132
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/error-signal"));
@@ -223,6 +256,50 @@ private static class Foo {
223256
}
224257

225258

259+
@Configuration
260+
@SuppressWarnings({"unused", "WeakerAccess"})
261+
static class StaticResourceConfig {
262+
263+
@Bean
264+
public SimpleUrlHandlerMapping resourceMapping(ResourceWebHandler resourceWebHandler) {
265+
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
266+
mapping.setUrlMap(Map.of("/resources/**", resourceWebHandler));
267+
return mapping;
268+
}
269+
270+
@Bean
271+
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
272+
return new RequestMappingHandlerAdapter();
273+
}
274+
275+
@Bean
276+
public SimpleHandlerAdapter simpleHandlerAdapter() {
277+
return new SimpleHandlerAdapter();
278+
}
279+
280+
@Bean
281+
public ResourceWebHandler resourceWebHandler() {
282+
return new ResourceWebHandler();
283+
}
284+
285+
@Bean
286+
public ResponseEntityResultHandler responseEntityResultHandler() {
287+
ServerCodecConfigurer configurer = ServerCodecConfigurer.create();
288+
return new ResponseEntityResultHandler(configurer.getWriters(), new HeaderContentTypeResolver());
289+
}
290+
291+
@Bean
292+
GlobalExceptionHandler globalExceptionHandler() {
293+
return new GlobalExceptionHandler();
294+
}
295+
}
296+
297+
298+
@ControllerAdvice
299+
private static class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
300+
}
301+
302+
226303
private static class ServerError500ExceptionHandler implements WebExceptionHandler {
227304

228305
@Override

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 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.
@@ -26,6 +26,7 @@
2626
import org.springframework.core.Ordered;
2727
import org.springframework.core.log.LogFormatUtils;
2828
import org.springframework.lang.Nullable;
29+
import org.springframework.util.ObjectUtils;
2930
import org.springframework.util.StringUtils;
3031
import org.springframework.web.servlet.HandlerExceptionResolver;
3132
import org.springframework.web.servlet.ModelAndView;
@@ -98,6 +99,25 @@ public void setMappedHandlerClasses(Class<?>... mappedHandlerClasses) {
9899
this.mappedHandlerClasses = mappedHandlerClasses;
99100
}
100101

102+
/**
103+
* Add a mapped handler class.
104+
* @since 6.1
105+
*/
106+
public void addMappedHandlerClass(Class<?> mappedHandlerClass) {
107+
this.mappedHandlerClasses = (this.mappedHandlerClasses != null ?
108+
ObjectUtils.addObjectToArray(this.mappedHandlerClasses, mappedHandlerClass) :
109+
new Class<?>[] {mappedHandlerClass});
110+
}
111+
112+
/**
113+
* Return the {@link #setMappedHandlerClasses(Class[]) configured} mapped
114+
* handler classes.
115+
*/
116+
@Nullable
117+
protected Class<?>[] getMappedHandlerClasses() {
118+
return this.mappedHandlerClasses;
119+
}
120+
101121
/**
102122
* Set the log category for warn logging. The name will be passed to the underlying logger
103123
* implementation through Commons Logging, getting interpreted as a log category according

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

+7
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.springframework.web.servlet.View;
5757
import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver;
5858
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
59+
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
5960
import org.springframework.web.servlet.support.RequestContextUtils;
6061

6162
/**
@@ -372,6 +373,12 @@ protected boolean hasGlobalExceptionHandlers() {
372373
return !this.exceptionHandlerAdviceCache.isEmpty();
373374
}
374375

376+
@Override
377+
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
378+
return (handler instanceof ResourceHttpRequestHandler ?
379+
hasGlobalExceptionHandlers() : super.shouldApplyTo(request, handler));
380+
}
381+
375382
/**
376383
* Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception.
377384
*/

spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandlerIntegrationTests.java

+45-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.
@@ -19,22 +19,28 @@
1919
import java.io.IOException;
2020
import java.net.MalformedURLException;
2121
import java.nio.charset.StandardCharsets;
22+
import java.util.List;
2223
import java.util.stream.Stream;
2324

2425
import jakarta.servlet.ServletException;
26+
import org.junit.jupiter.api.Test;
2527
import org.junit.jupiter.params.ParameterizedTest;
2628
import org.junit.jupiter.params.provider.Arguments;
2729
import org.junit.jupiter.params.provider.MethodSource;
2830

2931
import org.springframework.core.io.ClassPathResource;
3032
import org.springframework.core.io.FileSystemResource;
3133
import org.springframework.core.io.UrlResource;
34+
import org.springframework.http.converter.HttpMessageConverter;
35+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
36+
import org.springframework.web.bind.annotation.ControllerAdvice;
3237
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
3338
import org.springframework.web.servlet.DispatcherServlet;
3439
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
3540
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
3641
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
3742
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
43+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
3844
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
3945
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
4046
import org.springframework.web.testfixture.servlet.MockServletConfig;
@@ -114,6 +120,34 @@ void classpathLocationWithEncodedPath(
114120
assertThat(response.getContentAsString()).as(description).isEqualTo("h1 { color:red; }");
115121
}
116122

123+
@Test
124+
void testNoResourceFoundException() throws Exception {
125+
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
126+
context.setServletConfig(this.servletConfig);
127+
context.register(WebConfig.class);
128+
context.register(GlobalExceptionHandler.class);
129+
context.refresh();
130+
131+
DispatcherServlet servlet = new DispatcherServlet();
132+
servlet.setApplicationContext(context);
133+
servlet.init(this.servletConfig);
134+
135+
MockHttpServletRequest request = initRequest("/cp/non-existing");
136+
MockHttpServletResponse response = new MockHttpServletResponse();
137+
138+
servlet.service(request, response);
139+
140+
assertThat(response.getStatus()).isEqualTo(404);
141+
assertThat(response.getContentType()).isEqualTo("application/problem+json");
142+
assertThat(response.getContentAsString()).isEqualTo("""
143+
{"type":"about:blank",\
144+
"title":"Not Found",\
145+
"status":404,\
146+
"detail":"No static resource non-existing.",\
147+
"instance":"/cp/non-existing"}\
148+
""");
149+
}
150+
117151
private DispatcherServlet initDispatcherServlet(
118152
boolean usePathPatterns, boolean decodingUrlPathHelper, Class<?>... configClasses) throws ServletException {
119153

@@ -176,6 +210,11 @@ private UrlResource urlResource(String path) {
176210
}
177211
return urlResource;
178212
}
213+
214+
@Override
215+
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
216+
converters.add(new MappingJackson2HttpMessageConverter());
217+
}
179218
}
180219

181220

@@ -209,4 +248,9 @@ public void configurePathMatch(PathMatchConfigurer configurer) {
209248
}
210249
}
211250

251+
252+
@ControllerAdvice
253+
private static class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
254+
}
255+
212256
}

0 commit comments

Comments
 (0)