前言
最近做项目,需要通过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 errorrequest.protocol().setString(Constants.HTTP_11);// %nn decoding will be checked at the point of decodingString 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, 127if (i < 32 || i == 127) {IS_CONTROL[i] = true;}// Separatorif (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 separatorif (!IS_CONTROL[i] && !IS_SEPARATOR[i] && i < 128) {IS_TOKEN[i] = true;}// Hex: 0-9, a-f, A-Fif ((i >= '0' && i <= '9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) {IS_HEX[i] = true;}// Not valid for HTTP protocol// "HTTP/" DIGIT "." DIGITif (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标准的基础上转换了各种类型,实现了方便快速的开发。