Skip to content

Commit 99c4a9e

Browse files
committed
Filtering for nested ERROR dispatch
OncePerRequestFilter now has a doFilter method that allows separate processing of nested ERROR dispatches. This is useful for filters that wrap the request and response. Closes gh-23196
1 parent 235858e commit 99c4a9e

File tree

3 files changed

+229
-18
lines changed

3 files changed

+229
-18
lines changed

spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
158158
}
159159
}
160160

161+
@Override
162+
protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response,
163+
FilterChain filterChain) throws ServletException, IOException {
164+
165+
doFilterInternal(request, response, filterChain);
166+
}
161167

162168
/**
163169
* Hide "Forwarded" or "X-Forwarded-*" headers.

spring-web/src/main/java/org/springframework/web/filter/OncePerRequestFilter.java

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,19 @@ public final void doFilter(ServletRequest request, ServletResponse response, Fil
9494
HttpServletResponse httpResponse = (HttpServletResponse) response;
9595

9696
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
97-
alreadyFilteredAttributeName = updateForErrorDispatch(alreadyFilteredAttributeName, request);
9897
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
9998

100-
if (hasAlreadyFilteredAttribute || skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
99+
if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
100+
101+
// Proceed without invoking this filter...
102+
filterChain.doFilter(request, response);
103+
}
104+
else if (hasAlreadyFilteredAttribute) {
105+
106+
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
107+
doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
108+
return;
109+
}
101110

102111
// Proceed without invoking this filter...
103112
filterChain.doFilter(request, response);
@@ -115,7 +124,6 @@ public final void doFilter(ServletRequest request, ServletResponse response, Fil
115124
}
116125
}
117126

118-
119127
private boolean skipDispatch(HttpServletRequest request) {
120128
if (isAsyncDispatch(request) && shouldNotFilterAsyncDispatch()) {
121129
return true;
@@ -167,21 +175,6 @@ protected String getAlreadyFilteredAttributeName() {
167175
return name + ALREADY_FILTERED_SUFFIX;
168176
}
169177

170-
private String updateForErrorDispatch(String alreadyFilteredAttributeName, ServletRequest request) {
171-
172-
// Jetty does ERROR dispatch within sendError, so request attribute is still present
173-
// Use a separate attribute for ERROR dispatches
174-
175-
if (DispatcherType.ERROR.equals(request.getDispatcherType()) && !shouldNotFilterErrorDispatch() &&
176-
request.getAttribute(alreadyFilteredAttributeName) != null) {
177-
178-
return alreadyFilteredAttributeName + ".ERROR";
179-
}
180-
181-
182-
return alreadyFilteredAttributeName;
183-
}
184-
185178
/**
186179
* Can be overridden in subclasses for custom filtering control,
187180
* returning {@code true} to avoid filtering of the given request.
@@ -238,4 +231,23 @@ protected abstract void doFilterInternal(
238231
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
239232
throws ServletException, IOException;
240233

234+
/**
235+
* Typically an ERROR dispatch happens after the REQUEST dispatch completes,
236+
* and the filter chain starts anew. On some servers however the ERROR
237+
* dispatch may be nested within the REQUEST dispatch, e.g. as a result of
238+
* calling {@code sendError} on the response. In that case we are still in
239+
* the filter chain, on the same thread, but the request and response have
240+
* been switched to the original, unwrapped ones.
241+
* <p>Sub-classes may use this method to filter such nested ERROR dispatches
242+
* and re-apply wrapping on the request or response. {@code ThreadLocal}
243+
* context, if any, should still be active as we are still nested within
244+
* the filter chain.
245+
* @since 5.1.9
246+
*/
247+
protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response,
248+
FilterChain filterChain) throws ServletException, IOException {
249+
250+
doFilter(request, response, filterChain);
251+
}
252+
241253
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
* Copyright 2002-2019 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+
package org.springframework.web.filter;
17+
18+
import java.io.IOException;
19+
import javax.servlet.DispatcherType;
20+
import javax.servlet.FilterChain;
21+
import javax.servlet.ServletException;
22+
import javax.servlet.http.HttpServlet;
23+
import javax.servlet.http.HttpServletRequest;
24+
import javax.servlet.http.HttpServletResponse;
25+
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
29+
import org.springframework.mock.web.test.MockFilterChain;
30+
import org.springframework.mock.web.test.MockHttpServletRequest;
31+
import org.springframework.mock.web.test.MockHttpServletResponse;
32+
import org.springframework.web.util.WebUtils;
33+
34+
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertFalse;
36+
import static org.junit.Assert.assertTrue;
37+
38+
/**
39+
* Unit tests for {@link OncePerRequestFilter}.
40+
* @author Rossen Stoyanchev
41+
* @since 5.1.9
42+
*/
43+
public class OncePerRequestFilterTests {
44+
45+
private final TestOncePerRequestFilter filter = new TestOncePerRequestFilter();
46+
47+
private MockHttpServletRequest request;
48+
49+
private MockFilterChain filterChain;
50+
51+
52+
@Before
53+
@SuppressWarnings("serial")
54+
public void setup() throws Exception {
55+
this.request = new MockHttpServletRequest();
56+
this.request.setScheme("http");
57+
this.request.setServerName("localhost");
58+
this.request.setServerPort(80);
59+
this.filterChain = new MockFilterChain(new HttpServlet() {});
60+
}
61+
62+
63+
@Test
64+
public void filterOnce() throws ServletException, IOException {
65+
66+
// Already filtered
67+
this.request.setAttribute(this.filter.getAlreadyFilteredAttributeName(), Boolean.TRUE);
68+
69+
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
70+
assertFalse(this.filter.didFilter);
71+
assertFalse(this.filter.didFilterNestedErrorDispatch);
72+
73+
// Remove already filtered
74+
this.request.removeAttribute(this.filter.getAlreadyFilteredAttributeName());
75+
this.filter.reset();
76+
77+
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
78+
assertTrue(this.filter.didFilter);
79+
assertFalse(this.filter.didFilterNestedErrorDispatch);
80+
}
81+
82+
@Test
83+
public void shouldNotFilterErrorDispatch() throws ServletException, IOException {
84+
85+
initErrorDispatch();
86+
87+
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
88+
assertFalse(this.filter.didFilter);
89+
assertFalse(this.filter.didFilterNestedErrorDispatch);
90+
}
91+
92+
@Test
93+
public void shouldNotFilterNestedErrorDispatch() throws ServletException, IOException {
94+
95+
initErrorDispatch();
96+
this.request.setAttribute(this.filter.getAlreadyFilteredAttributeName(), Boolean.TRUE);
97+
98+
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
99+
assertFalse(this.filter.didFilter);
100+
assertFalse(this.filter.didFilterNestedErrorDispatch);
101+
}
102+
103+
@Test // gh-23196
104+
public void filterNestedErrorDispatch() throws ServletException, IOException {
105+
106+
// Opt in for ERROR dispatch
107+
this.filter.setShouldNotFilterErrorDispatch(false);
108+
109+
this.request.setAttribute(this.filter.getAlreadyFilteredAttributeName(), Boolean.TRUE);
110+
initErrorDispatch();
111+
112+
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
113+
assertFalse(this.filter.didFilter);
114+
assertTrue(this.filter.didFilterNestedErrorDispatch);
115+
}
116+
117+
private void initErrorDispatch() {
118+
this.request.setDispatcherType(DispatcherType.ERROR);
119+
this.request.setAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE, "/error");
120+
}
121+
122+
123+
private static class TestOncePerRequestFilter extends OncePerRequestFilter {
124+
125+
private boolean shouldNotFilter;
126+
127+
private boolean shouldNotFilterAsyncDispatch = true;
128+
129+
private boolean shouldNotFilterErrorDispatch = true;
130+
131+
private boolean didFilter;
132+
133+
private boolean didFilterNestedErrorDispatch;
134+
135+
136+
public void setShouldNotFilter(boolean shouldNotFilter) {
137+
this.shouldNotFilter = shouldNotFilter;
138+
}
139+
140+
public void setShouldNotFilterAsyncDispatch(boolean shouldNotFilterAsyncDispatch) {
141+
this.shouldNotFilterAsyncDispatch = shouldNotFilterAsyncDispatch;
142+
}
143+
144+
public void setShouldNotFilterErrorDispatch(boolean shouldNotFilterErrorDispatch) {
145+
this.shouldNotFilterErrorDispatch = shouldNotFilterErrorDispatch;
146+
}
147+
148+
149+
public boolean didFilter() {
150+
return this.didFilter;
151+
}
152+
153+
public boolean didFilterNestedErrorDispatch() {
154+
return this.didFilterNestedErrorDispatch;
155+
}
156+
157+
public void reset() {
158+
this.didFilter = false;
159+
this.didFilterNestedErrorDispatch = false;
160+
}
161+
162+
163+
@Override
164+
protected boolean shouldNotFilter(HttpServletRequest request) {
165+
return this.shouldNotFilter;
166+
}
167+
168+
@Override
169+
protected boolean shouldNotFilterAsyncDispatch() {
170+
return this.shouldNotFilterAsyncDispatch;
171+
}
172+
173+
@Override
174+
protected boolean shouldNotFilterErrorDispatch() {
175+
return this.shouldNotFilterErrorDispatch;
176+
}
177+
178+
@Override
179+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
180+
FilterChain filterChain) {
181+
182+
this.didFilter = true;
183+
}
184+
185+
@Override
186+
protected void doFilterNestedErrorDispatch(HttpServletRequest request, HttpServletResponse response,
187+
FilterChain filterChain) {
188+
189+
this.didFilterNestedErrorDispatch = true;
190+
}
191+
}
192+
193+
}

0 commit comments

Comments
 (0)