Poison

FailedRequestFilter

今天帮同事查了个参数丢失的问题,确认是由参数大小超过 Tomcat 默认限制引起,源码位于 tomcat/Request.java:

1
2
3
4
5
6
7
8
9
10
if ((maxPostSize >= 0) && (len > maxPostSize)) {
Context context = getContext();
if (context != null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.postTooLarge"));
}
checkSwallowInput();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
return;
}

业务层的感知就是参数莫名丢失了,查询了下 Tomcat 为何仅设置了参数解析失败的标志而为何未抛出相关异常。其实 Tomcat 是提供了过滤器 FailedRequestFilter 向客户端返回相关异常信息的,源码位于 tomcat/FailedRequestFilter.java at 8.5.82:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* Filter that will reject requests if there was a failure during parameter
* parsing. This filter can be used to ensure that none parameter values
* submitted by client are lost.
*
* <p>
* Note that parameter parsing may consume the body of an HTTP request, so
* caution is needed if the servlet protected by this filter uses
* <code>request.getInputStream()</code> or <code>request.getReader()</code>
* calls. In general the risk of breaking a web application by adding this
* filter is not so high, because parameter parsing does check content type
* of the request before consuming the request body.
*/
public class FailedRequestFilter extends FilterBase {

// Log must be non-static as loggers are created per class-loader and this
// Filter may be used in multiple class loaders
private final Log log = LogFactory.getLog(FailedRequestFilter.class); // must not be static

@Override
protected Log getLogger() {
return log;
}

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (!isGoodRequest(request)) {
FailReason reason = (FailReason) request.getAttribute(
Globals.PARAMETER_PARSE_FAILED_REASON_ATTR);

int status;

switch (reason) {
case IO_ERROR:
// Not the client's fault
status = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
break;
case POST_TOO_LARGE:
status = HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE;
break;
case TOO_MANY_PARAMETERS:
// 413/414 aren't really correct here since the request body
// and/or URI could be well below any limits set. Use the
// default.
case UNKNOWN: // Assume the client is at fault
// Various things that the client can get wrong that don't have
// a specific status code so use the default.
case INVALID_CONTENT_TYPE:
case MULTIPART_CONFIG_INVALID:
case NO_NAME:
case REQUEST_BODY_INCOMPLETE:
case URL_DECODING:
case CLIENT_DISCONNECT:
// Client is never going to see this so this is really just
// for the access logs. The default is fine.
default:
// 400
status = HttpServletResponse.SC_BAD_REQUEST;
break;
}

((HttpServletResponse) response).sendError(status);
return;
}
chain.doFilter(request, response);
}

private boolean isGoodRequest(ServletRequest request) {
// Trigger parsing of parameters
request.getParameter("none");
// Detect failure
if (request.getAttribute(Globals.PARAMETER_PARSE_FAILED_ATTR) != null) {
return false;
}
return true;
}

@Override
protected boolean isConfigProblemFatal() {
return true;
}

}

但是该过滤器默认情况下并未启用。根据注释可以知道,该过滤器可能消费 HTTP 请求体,所以使用了 request.getInputStream()request.getReader() 的 Servlet 需要小心使用该过滤器。其中该过滤器源码中的 log 引起了我的注意,特别是该段注释:

Log must be non-static as loggers are created per class-loader and this Filter may be used in multiple class loaders

关于 log 的提交位于 Logs for Filters must be non-static as loggers are created per class-… · apache/tomcat@dd44360 · GitHub,在该次提交的 changelog 中也只是提到是为了处理 reload 这种场景:

Make all loggers associated with Tomcat provided Filters non-static to ensure that log messages are not lost when a web application is reloaded.

经过询问后明确 log 未使用 static 修饰的原因如下:

When Webapp classloader loads the Filter class, it does not “load the class” (i.e. does not read the bytes from a jar archive and does not produce a Class out of those bytes). It delegates the task to its parent in the classloader hierarchy. Thus, the Filter class is loaded by the Common classloader.

At the same time, the logging configuration for each web application may be different:
A web application may provide its own copy of logging.properties file by placing it into its own WEB-INF/classes/ directory.

FailedRequestFilter 是 Tomcat 内部的类,经过 Webapp classLoader 的委托后实际是由 Common classLoader 进行加载,所以只会被加载一次,如果 log 被 static 修饰了,那么如果 webapp 中的日志配置变更后,因为 log 实例未被重建则会导致最新的日志配置不能生效。所以将其声明为非静态是为了保证每次实例化 FailedRequestFilter 时也重新实例化 log 实例以保证 webapp 中的最新日志配置生效。

Reference

Classloader Hierarchy for Tomcat
Question about the log variable in Filters