URI中+号表示空格,这一点已经是常识般的存在。但是还是存在很多令人困扰的地方,并带来许多BUG。

首先是一些Java类,甚至是JDK自带的URI类令人迷惑的表现:

1
2
3
4
5
6
7
URI uri = new URI(null, null, "1%+ 2", "a=%+ b", null);
System.out.println(uri); // 1%25+%202?a=%25+%20b

URIBuilder uriBuilder = new URIBuilder();
uriBuilder.setPath("1%+ 2");
uriBuilder.addParameter("a","%+ b");
System.out.println(uriBuilder); // 1%25+%202?a=%25%2B+b

可以看到JDK的URI类和HttpClient的URIBuild类对同样的输入,编码不一样:

  • 对于URI的path部分,两者都保留了+号不做编码,把空格编码为%20
  • 对于URI的query部分
    • JDK的URI类:对+号不做编码,空格编码为%20
    • HttpClient的URIBuild类:+号编码为%2B,空格编码为+

可以看到,即使是应该被认为是客观正确的类,在处理+和空格上面,表现差距很大,大到生成的URI的语义不一样了。

那回头来看,URI中+号表示空格,这个到底正确么?还是一个错误的常识?

查阅了一些资料,比较令人信服的说法是:

  • 在URI的path中,+号表示加号
  • 在URI的query部分中,+号表示空格

具体讨论见这篇问答。其中作者提到,RFC中,只是定义了URI的各个component允许存在哪些字符,但是对于特定的字符是否有特殊的含义,是没有做出说明的。而在HTTP的规范中,对application/x-www-form-urlencoded进行说明时,提到了replace spaces with + and other special characters as in RFC1738

也就是说在URI的RFC规范中,没有定义+号表示空格,是指说了+可以在path和query中存在。而HTTP规范中,在参数编码时,提到了空格编码为+号,后者被大规模接受了。

而Java的URI来看起来是是完全按照URI的RFC规范来实现,其在编码各个component时,只有不允许存在的字符才会进行编码,+是允许存在的,所以都没有进行编码。这样带来的问题是,query中的+号没有被编码,导致访问时异常。

URIBuilder的编码,在编码却query时是按照application/x-www-form-urlencoded规则进行的,所以编码出的地址是比较正确的。

回头看另外一个问题,就是常识中,我们认为URI中+号表示空格,但是其实path中的+号表示加号本身。这个就麻烦了,因为很多时候,我们在进行Java web编码的时候,会使用URLEncoderURLDecoder类来处理uri的path,这两个类使用的是application/x-www-form-urlencoded规则,会用+号表示空格,这样在处理path时就不正确了。

细节好多,感觉分分钟都能踩坑,更别提很多人对URI编码没有概念,代码中大量存在用字符串拼接的方式拼接URI,这种方式的结果就是,带来大量的BUG。。。

但是即使是S3,也没有正确处理好URI。。。这个让我非常意外,在AWS的论坛(帖子地址)上有人提到S3对path中的+号是按照空格处理的!我试了一下还真是。。

1
2
3
4
5
6
7
8
# curl -i "https://xx.s3-eu-west-1.amazonaws.com/1+%2B2.txt"
HTTP/1.1 200 OK

# curl -i "https://xx.s3-eu-west-1.amazonaws.com/1%20%2B2.txt"
HTTP/1.1 200 OK

# curl -i "https://xx.s3-eu-west-1.amazonaws.com/1%2B%2B2.txt"
HTTP/1.1 403 Forbidden

在这个帖子中,有AWS的员工回复,表示的确有这个问题,但是因为有大量的这种地址存在了,所以不会去修改。。。