前言
最近做项目,需要通过GET传参,来实现查询的能力,本来是RPC调用,直接参数序列化即可。但是服务最近修改为HTTP,本来Spring Cloud的feign也可以直接传参数,但是当使用Nginx访问时参数到底传啥呢,笔者传入?list=['xxx']直接就报错了,错误类型
Invalid character found in the request target [/haha?list=[%27haha%27] ]. The valid characters are defined in RFC 7230 and RFC 3986
准备demo
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.18</version>
</dependency>
demo随意写,写个springboot项目,实际上跟servlet容器有关,Boot默认Tomcat,其他servlet容器可能实现标准不一样而不同。
@GetMapping("/haha")
public String sayHello(List<String> list){
return list.toString();
}
如果我们自己实现http get,实际上就是针对url的参数解析,说不定?list=['xxx']就不会报错,追根溯源--标准的实现区别。
日志如下
源码分析
查询资料:RFC 7230: Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routinghttps://www.rfc-editor.org/rfc/rfc7230
这个标准定义了400状态码http 1.1协议的一些规则
RFC 3986: Uniform Resource Identifier (URI): Generic Syntax https://www.rfc-editor.org/rfc/rfc3986 定义了保留字符
很不幸[]属于保留字符。
从Tomcat的源码看org.apache.coyote.http11.Http11InputBuffer的parseRequestLine方法。
} else if (parsingRequestLineQPos != -1 && !httpParser.isQueryRelaxed(chr)) {
// Avoid unknown protocol triggering an additional error
request.protocol().setString(Constants.HTTP_11);
// %nn decoding will be checked at the point of decoding
String invalidRequestTarget = parseInvalid(parsingRequestLineStart, byteBuffer);
throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget", invalidRequestTarget));
}
逐个字符读取。发现是保留字符则根据标准抛出异常,提示根据什么标准。
在org.apache.tomcat.util.http.parser.HttpParser的静态代码块中,定义了通常不允许的字符。
static {
for (int i = 0; i < ARRAY_SIZE; i++) {
// Control> 0-31, 127
if (i < 32 || i == 127) {
IS_CONTROL[i] = true;
}
// Separator
if (i == '(' || i == ')' || i == '<' || i == '>' || i == '@' || i == ',' || i == ';' || i == ':' ||
i == '\\' || i == '\"' || i == '/' || i == '[' || i == ']' || i == '?' || i == '=' || i == '{' ||
i == '}' || i == ' ' || i == '\t') {
IS_SEPARATOR[i] = true;
}
// Token: Anything 0-127 that is not a control and not a separator
if (!IS_CONTROL[i] && !IS_SEPARATOR[i] && i < 128) {
IS_TOKEN[i] = true;
}
// Hex: 0-9, a-f, A-F
if ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) {
IS_HEX[i] = true;
}
// Not valid for HTTP protocol
// "HTTP/" DIGIT "." DIGIT
if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) {
IS_HTTP_PROTOCOL[i] = true;
}
if (i >= '0' && i <= '9') {
IS_NUMERIC[i] = true;
}
if (i >= 'a' && i <= 'z' || i >= 'A' && i <= 'Z') {
IS_ALPHA[i] = true;
}
if (IS_ALPHA[i] || IS_NUMERIC[i] || i == '+' || i == '-' || i == '.') {
IS_SCHEME[i] = true;
}
if (IS_ALPHA[i] || IS_NUMERIC[i] || i == '-' || i == '.' || i == '_' || i == '~') {
IS_UNRESERVED[i] = true;
}
if (i == '!' || i == '$' || i == '&' || i == '\'' || i == '(' || i == ')' || i == '*' || i == '+' ||
i == ',' || i == ';' || i == '=') {
IS_SUBDELIM[i] = true;
}
// userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
if (IS_UNRESERVED[i] || i == '%' || IS_SUBDELIM[i] || i == ':') {
IS_USERINFO[i] = true;
}
// The characters that are normally not permitted for which the
// restrictions may be relaxed when used in the path and/or query
// string
// 明确定义通常不允许使用的字符,当然也可以放开限制,当为路径或者查询参数
if (i == '\"' || i == '<' || i == '>' || i == '[' || i == '\\' || i == ']' || i == '^' || i == '`' ||
i == '{' || i == '|' || i == '}') {
IS_RELAXABLE[i] = true;
}
}
DEFAULT = new HttpParser(null, null);
}
至此我们知道了数组和集合使用get传参报错的原因:明确定义通常不允许使用的字符,当然也可以放开限制,当为路径或者查询参数。
解决办法
解决办法也简单了,解析参数规避'[' ']'这样的字符即可,比如string默认tolist,使用string,string传list或者数组对象等,解析规则实际上我们甚至可以自定义,参考Tomcat或者springboot的规则。
比如使用字符串:实际上springboot就是这么做的,tomcat毕竟GET仅传递String字段,各种参数类型都是后面Springboot转换的
如果使用List直接注入,那么因为没有构造函数,报错,毕竟接口类型,反射无法初始化对象。
因为Springboot会反射解析对象,所以即使使用ArrayList也不能解析参数,因为默认情况仅能解析String
只有@RequestParam绑定参数key才行
因为解析器不一样,具体就不分析了,涉及Springmvc的设计。
数组或者集合使用,分割。
实际上还有其他办法:我们可以放开限制,只需要注入HttpParser即可
在tomcat协议定义里面org.apache.coyote.http11.AbstractHttp11Protocol
定义了
那么我们只要注入这2个值即可,第一个是路径字符,第2个是查询字符,根据最小修改原则,此处注入查询参数。根据Springboot自动装配org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration的属性注入即可
application.properties文件
server.tomcat.relaxedQueryChars=<,>,[,\,],^,`,{,|,}
即可实现放开限制
但是tomcat认为'[' ']'是字符串的字符,以,逗号分割。
并不符合我们的直观感受,所以还是一个原则,只不过是允许'[' ']'这样的字符了。
总结
通过tomcat GET传参数,尤其是数组或者集合,发现tomcat实现了很多开源标准,预估其他servlet容器也差不多,如果是我们自己实现servlet容器解析http协议包,那么这些标准估计是不会去实现的,开源的能力定义了一系列标准并且基本上都实现了。而Springboot在tomcat标准的基础上转换了各种类型,实现了方便快速的开发。