记一次Nginx+Tomcat地址有特殊符号的时候报400的情况

遇到一个奇葩的问题, nginx访问Tomcat的时候, 我的请求地址中有中括号, 就会出现400的问题, 重点页面没有任何错误提示, 而且后台也没有任何错误日志.

(English version translate by GPT-3.5)

问题还原

我的nginx配置如下

1
2
3
4
5
6
7
location /files/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8080/apps/files/;
}

Tomcat是使用Servlet的方式进行接收

1
2
3
4
@WebServlet(name = "FileServlet", urlPatterns = "/file/*", loadOnStartup = 1)
public class FileServlet extends BaseServlet {
...
}

这样的情况下, 也就是, 如下的请求, 这是没有任何问题的

1
2
3
http://host/files/folder1/folder2
->
http://127.0.0.1:8080/apps/files/folder1/folder2

如果这样子的话, 就有问题了

1
2
3
http://host/files/folder1/folder2/01-[1]-file.file
->
http://127.0.0.1:8080/apps/files/folder1/folder2/01-[1]-file.file

出现的错误页面

error

而这个页面没有任何的错误提示, 但是如果我将中括号改成大括号会有如下报错.

error2

处理过程

搜搜搜

我google了半天, 找到几个, 大部分说在proxy_pass中删掉URI, 还有几个是说.

  1. request header过大
  2. proxy_set_header Host $host没有加

但是我这2个问题都没出现, 但是我的请求地址和header小得很, 以及我所有都加了Host这一条.

开Tomcat Debug

还是自己解决把, 因为没有错误日志, 连servlet都没收到请求, 我就开tomcat的Debug日志, 开debug很简单, 在/${tomcatRoot}/conf/logging.properties简单加这一行就行了

1
.level = FINE

我添加在了如下的位置

1
2
3
4
5
6
7
8
9
10
11
...
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################
# 增加下面这一行, 其他不用改
.level = FINE
# 增加上面这一行, 其他不用改
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs
...

这样子启动tomcat, 会打印超多超多的日志, 然后我再次请求, 看到了这一条

1
2
3
4
5
6
7
8
9
10
11
12
.....
org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper.read Socket: [org.apache.tomcat.util.net.NioEndpoint$NioSocketWrapper@7de71b73:org.apache.tomcat.util.net.NioChannel@657aef19:java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:47234]], Read direct from socket: [743]
10-Feb-2020 20:32:26.697 FINE [http-nio-8080-exec-2] org.apache.coyote.http11.Http11InputBuffer.parseRequestLine Received [GET
# 这一行
http://127.0.0.1:8080/apps/files/folder1/folder2/01-[1]-file.file HTTP/1.0
# 这一行
Host: host
X-Real-IP: xxx.xxx.xxx.xxx
X-Forwarded-Proto: https
X-Forwarded-For: xxx.xxx.xxx.xxx
Connection: close
....

但是我此时传入的这个url浏览器已经经过了urlEncode, 然后我尝试把后面的01-[1]-file.file再次进行二次url, 变成01-%255B1%255D-file.file(urlEncode2次), 然后看到请求是

1
2
3
4
5
6
7
8
9
10
11
...
10-Feb-2020 20:36:50.698 FINE [http-nio-8080-exec-1] org.apache.coyote.http11.Http11InputBuffer.parseRequestLine Received [GET
# 这一行
http://127.0.0.1:8080/apps/files/folder1/folder2/01-%255B1%255D-file.file HTTP/1.0
# 这一行
Host: host
X-Real-IP: xxx.xxx.xxx.xxx
X-Forwarded-Proto: https
X-Forwarded-For: xxx.xxx.xxx.xxx
Connection: close
...

发现和浏览器的url地址是完全一致, 因此我觉得nginx没有对第二个进行解码, 而第一个nginx进行了解码后反代

找到问题

我看了下官网的proxy_pass的文档, Module ngx_http_proxy_module, 看到如下的内容, 就简单说下重要的部分


如果 proxy_pass 指向了一个指定的URL地址, 会对匹配的URL进行替换, 然后请求到后端Web服务.

location /name/ {
proxy_pass http://127.0.0.1/remote/;
}

即请求 http://host/name/aaa -> http://127.0.0.1/remote/aaa

如果 proxy_pass 没有携带任何URI, 那么**地址以原始请求(即不做处理和匹配)**的形式请求后端Web服务, 或者在处理更改的URI时传递完整的规范化请求URI:

location /some/path/ {
proxy_pass http://127.0.0.1;
}

即请求 http://host/some/path/ -> http://127.0.0.1/some/path/

当在proxy_pass中使用了变量时

location /name/ {
proxy_pass http://127.0.0.1$request_uri;
}

即请求 http://host/name/aaa/bbb -> http://127.0.0.1/name/aaa/bbb

像这样的情况下, 会将URI原样的传递到服务器

解决

所以, 按照文档, 我的解决方案有以下2个(大部分说的是第一个)

1
2
3
4
5
6
7
location /apps/files/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8080;
}

或者是

1
2
3
4
5
6
7
location /apps {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://127.0.0.1:8080$request_uri;
}

对于第一种方案, 如果我访问/apps/image就要再配置一次, 因此, 第二个是满足了我的要求的, 但是, 如果是以下方式, 我暂时还没答案

这个就不会了…

1
2
3
4
5
http://host/content/folder1/folder2/01[1].file
->
http://127.0.0.1:8080/files/folder1/folder2/01[1].file

因为如国按照第一个配置, 我的tomcat的访问必须是/content, 如果使用第二种就变成了http://127.0.0.1:8080/content/folder1/folder2/01[1].file, 而tomcat中也没有/content