Skip to content

Commit 8e61d4e

Browse files
committed
PreMarching filter errors should be filtered as ordinary request filters
1 parent ac5020f commit 8e61d4e

File tree

2 files changed

+179
-12
lines changed

2 files changed

+179
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2017-2023 original 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+
package io.micronaut.http.server.tck.tests.filter;
17+
18+
import io.micronaut.context.annotation.Requires;
19+
import io.micronaut.http.HttpRequest;
20+
import io.micronaut.http.HttpResponse;
21+
import io.micronaut.http.HttpStatus;
22+
import io.micronaut.http.MediaType;
23+
import io.micronaut.http.MutableHttpRequest;
24+
import io.micronaut.http.MutableHttpResponse;
25+
import io.micronaut.http.annotation.Controller;
26+
import io.micronaut.http.annotation.Get;
27+
import io.micronaut.http.annotation.Produces;
28+
import io.micronaut.http.annotation.RequestFilter;
29+
import io.micronaut.http.annotation.ResponseFilter;
30+
import io.micronaut.http.annotation.ServerFilter;
31+
import io.micronaut.http.server.annotation.PreMatching;
32+
import io.micronaut.http.server.exceptions.ExceptionHandler;
33+
import io.micronaut.http.tck.AssertionUtils;
34+
import io.micronaut.http.tck.BodyAssertion;
35+
import io.micronaut.http.tck.HttpResponseAssertion;
36+
import io.micronaut.http.tck.TestScenario;
37+
import io.micronaut.web.router.RouteInfo;
38+
import io.micronaut.web.router.RouteMatch;
39+
import jakarta.inject.Singleton;
40+
import org.junit.jupiter.api.Test;
41+
42+
import java.io.IOException;
43+
44+
import static io.micronaut.http.annotation.Filter.MATCH_ALL_PATTERN;
45+
46+
@SuppressWarnings({
47+
"java:S5960", // We're allowed assertions, as these are used in tests only
48+
"checkstyle:MissingJavadocType",
49+
"checkstyle:DesignForExtension"
50+
})
51+
public class ResponseFilterOnPreMatchExceptionHandlerTest {
52+
private static final String SPEC_NAME = "ResponseFilterOnPreMatchExceptionHandlerTest";
53+
54+
@Test
55+
public void exceptionHandlerTest() throws IOException {
56+
TestScenario.builder()
57+
.specName(SPEC_NAME)
58+
.request(HttpRequest.GET("/foo"))
59+
.assertion((server, request) -> AssertionUtils.assertDoesNotThrow(server, request, HttpResponseAssertion.builder()
60+
.status(HttpStatus.OK)
61+
.body(BodyAssertion.builder().body("EXCEPTION MAPPER RESPONSE #2 FILTER RESPONSE #1 FILTER").equals())
62+
.build()))
63+
.run();
64+
}
65+
66+
static class FooException extends RuntimeException {
67+
68+
}
69+
70+
@ServerFilter(MATCH_ALL_PATTERN)
71+
@Requires(property = "spec.name", value = SPEC_NAME)
72+
static class ErrorThrowingFilter {
73+
@PreMatching
74+
@RequestFilter
75+
public void onPreMatching(MutableHttpRequest<?> request) {
76+
throw new FooException();
77+
}
78+
79+
@ResponseFilter
80+
public void onResponseWithRouteMatch(RouteMatch<?> routeMatch, MutableHttpResponse<?> response) {
81+
response.body(response.getBody(String.class).get() + " XYZ");
82+
}
83+
84+
@ResponseFilter
85+
public void onResponse1(MutableHttpResponse<?> response) {
86+
response.body(response.getBody(String.class).get() + " RESPONSE #1 FILTER");
87+
}
88+
89+
@ResponseFilter
90+
public void onResponse2(MutableHttpResponse<?> response) {
91+
response.body(response.getBody(String.class).get() + " RESPONSE #2 FILTER");
92+
}
93+
94+
@ResponseFilter
95+
public void onResponseWithRouteInfo(RouteInfo<?> routeInfo, MutableHttpResponse<?> response) {
96+
response.body(response.getBody(String.class).get() + " FOOBAR");
97+
}
98+
}
99+
100+
@Requires(property = "spec.name", value = SPEC_NAME)
101+
@Controller("/foo")
102+
static class FooController {
103+
@Produces(MediaType.TEXT_PLAIN)
104+
@Get
105+
String index() {
106+
return "Hello World";
107+
}
108+
}
109+
110+
@Requires(property = "spec.name", value = SPEC_NAME)
111+
@Singleton
112+
static class FooExceptionHandler implements ExceptionHandler<FooException, HttpResponse<?>> {
113+
114+
@Override
115+
public HttpResponse<?> handle(HttpRequest request, FooException exception) {
116+
return HttpResponse.ok("EXCEPTION MAPPER");
117+
}
118+
}
119+
}

http/src/main/java/io/micronaut/http/filter/FilterRunner.java

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,8 @@ public final ExecutionFlow<HttpResponse<?>> run(HttpRequest<?> request,
234234
} else {
235235
// Pre-matching filters plus route match resolver
236236
var f = new RouteMatchResolverHttpFilter();
237-
filtersToRun.add(f);
237+
filtersToRun.add(0, f); // Response filter to resolve filters on error
238+
filtersToRun.add(f); // Request filter to resolve filters after preMatch filters
238239
iterator = filtersToRun.listIterator();
239240
f.filterIterator = iterator;
240241
}
@@ -396,33 +397,80 @@ private ExecutionFlow<FilterContext> processFailureFilterException(FilterContext
396397
final class RouteMatchResolverHttpFilter implements InternalHttpFilter {
397398

398399
private ListIterator<InternalHttpFilter> filterIterator;
400+
private boolean executed = false;
401+
402+
@Override
403+
public boolean isFiltersResponse() {
404+
return true;
405+
}
399406

400407
@Override
401408
public boolean isFiltersRequest() {
402409
return true;
403410
}
404411

412+
@Override
413+
public ExecutionFlow<FilterContext> processResponseFilter(FilterContext context, Throwable exceptionToFilter) {
414+
if (!executed) {
415+
cleanupAllNext();
416+
addFoundFilters(context.request());
417+
moveIteratorToEnd();
418+
return ExecutionFlow.just(context);
419+
}
420+
return ExecutionFlow.just(context);
421+
}
422+
405423
@Override
406424
public ExecutionFlow<FilterContext> processRequestFilter(FilterContext context) {
425+
if (filterIterator.nextIndex() == 1) {
426+
return ExecutionFlow.just(context);
427+
}
428+
executed = true;
407429
HttpRequest<?> request = context.request();
408430
try {
409431
doRouteMatch(request);
410432
return ExecutionFlow.just(context);
411433
} catch (Throwable throwable) {
412434
return processFailurePropagateException(throwable, context);
413435
} finally {
436+
cleanupAllPrevious();
437+
addFoundFilters(request);
438+
moveIteratorToBeginning();
439+
}
440+
}
441+
442+
private void moveIteratorToBeginning() {
443+
while (filterIterator.hasPrevious()) {
444+
filterIterator.previous();
445+
}
446+
}
447+
448+
private void moveIteratorToEnd() {
449+
while (filterIterator.hasNext()) {
450+
filterIterator.next();
451+
}
452+
}
453+
454+
private void addFoundFilters(HttpRequest<?> request) {
455+
List<InternalHttpFilter> postFilters = findInternalFiltersAfterRouteMatch(request);
456+
for (InternalHttpFilter postFilter : postFilters) {
457+
filterIterator.add(postFilter);
458+
}
459+
}
460+
461+
private void cleanupAllNext() {
462+
filterIterator.remove();
463+
while (filterIterator.hasNext()) {
464+
filterIterator.next();
465+
filterIterator.remove();
466+
}
467+
}
468+
469+
private void cleanupAllPrevious() {
470+
filterIterator.remove();
471+
while (filterIterator.hasPrevious()) {
472+
filterIterator.previous();
414473
filterIterator.remove();
415-
while (filterIterator.hasPrevious()) {
416-
filterIterator.previous();
417-
filterIterator.remove();
418-
}
419-
List<InternalHttpFilter> postFilters = findInternalFiltersAfterRouteMatch(request);
420-
for (InternalHttpFilter postFilter : postFilters) {
421-
filterIterator.add(postFilter);
422-
}
423-
while (filterIterator.hasPrevious()) {
424-
filterIterator.previous();
425-
}
426474
}
427475
}
428476
}

0 commit comments

Comments
 (0)