4

由x-www-form-urlencoded引发的接口对接失败 - 扣钉日记

 1 year ago
source link: https://www.cnblogs.com/codelogs/p/17229438.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明。

问题发生#

这周正在写代码,突然,旁边小哥问我个问题...

  • 小哥:我这有个接口,自己调用没有问题,但别人调用就不行,这种问题该如何排查?
  • 我:抓下包看看呢...
  • 小哥:是这样使用tcpdump吗?

待小哥抓到包后,使用wireshark打开,并找到了相应的请求,类似如下:

fail

然后我让小哥将这个请求,使用curl发一个同样的请求,看能不能复现这个错误,如下:

$ curl -X POST localhost:80/api \
      -H 'Content-Type: application/x-www-form-urlencoded' \
      -d 'eyJvcmRlcl9pZCI6MTIzNDU2Nzg5MDIxNDN9Cg=='

命令执行之后,重现了调用方一样的接口报错。

然后抓包小哥自己的正确请求是这样的:

ok

这里很容易发现,别人调不通接口,小哥能调通,原因是别人的请求体里面缺失data=这一段😒

先不管为什么缺这个会报错,这里展示了一个实用技巧,对于http接口来说,排查这种接口调用差异问题,最直接高效的方法,就是对比正确调用与错误调用的数据包!

问题解决#

那么接下来,就是研究为什么报错了,看看服务端的处理代码,大概如下:

public JsonObject parseRequest(HttpServletRequest request, Charset charset) throws IOException {
      String base64Str = request.getParameter("data");
      if (base64Str == null) {
            try (InputStream is = request.getInputStream()) {
                  base64Str = StreamUtils.copyToString(is, charset);
            }
      }
      byte[] jsonBytes = Base64.getDecoder().decode(base64Str);
      return new Gson().toJsonTree(new String(jsonBytes, charset)).getAsJsonObject();
}

这个逻辑很简单,如下:

  1. 先从data参数中取数据。
  2. 若没有再从请求体中拿。
  3. 然后base64解码。
  4. 最后转json对象。

我们接口基本都这样,使用base64将数据包了一层,许多年过去了,具体原因不详,不深究😂

从上面处理逻辑看,按道理小哥的调用方式与别人的调用方式都是支持的,理论上来说,小哥的调用方式会命中request.getParameter,而别人的调用方式会命中request.getInputStream(),那为啥别人的调用方式不行?

小哥又调试了下上述服务端代码,发现使用别人的调用方式时,从request.getInputStream()中读不到数据😥

我在小哥旁边,提示将ContentType改成text/plain试试,curl命令改成这样:

$ curl -X POST localhost:80/api \
      -H 'Content-Type: text/plain' \
      -d 'eyJvcmRlcl9pZCI6MTIzNDU2Nzg5MDIxNDN9Cg=='

执行这条命令后,接口返回了正确结果😁

那为什么会这样呢😰😰😰

ContentType指的是什么?#

首先来看看ContentType指的是什么,看2个例子

  1. 如果ContentType是application/x-www-form-urlencoded时,请求可能是这样的:
    x-www-form-urlencoded
  2. 如果ContentType是application/json时,请求可能是这样的:
    json
  3. 如果ContentType是application/xml时,请求可能是这样的:
    xml

不难发现,ContentType这个请求头的作用是,指定请求体的数据格式。比如application/x-www-form-urlencoded表示请求体是key=value格式,application/json表示请求体是json格式,application/xml表示是xml格式,而text/plain表示请求体是纯文本。

那为什么将ContentType从application/x-www-form-urlencoded变成text/plain,报错的调用就能跑通了?

application/x-www-form-urlencoded有何不同?#

application/x-www-form-urlencoded是个历史非常悠久的ContentType了,它通过key=value的形式来组织表单数据,当然key和value还需要做urlencode编码。

而正是因为它如此悠久,所以被采纳在了web服务器的实现标准中,几乎所有的web服务器,当发现ContentType是application/x-www-form-urlencoded时,会自动按key=value&key2=value2的格式来解析请求体数据,解析完成后,我们就可以通过request.getParameter()来获取对应key的值了。

比如Tomcat的实现在org.apache.catalina.connector.Request#parseParameters,如下:

image_2023-03-17_20230317225055


解析key=value格式数据如下:

image_2023-03-17_20230317225236

但是,这里有一个重要的细节!

当ContentType是application/x-www-form-urlencoded时,由于Tomcat提前将请求体的数据流读了一遍,所以后面再通过request.getInputStream()就读不到请求体数据了。

如下,从request.getInputStream()中获取到的流,pos游标已经走到了lim结束位置了。

image_2023-03-17_20230317230241

而将ContentType改为text/plain后,Tomcat不会解析请求体,所以就不会读数据流,自然后面我们通过request.getInputStream()就又能读到数据了,故又可以调通了!

解决问题#

解决这个问题很简单,如下:

  1. 让调用方在请求体里加上data=,以符合application/x-www-form-urlencoded的key=value规范。
  2. 让调用方将ContentType修改为text/plain,因为调用方的请求数据就是base64纯文本而已,我们让调用方选择了这个方案。

如果调用方有很多,难以确定调用方的规范情况,那其实还有一种方案,通过request.getParameterMap()实现,代码有点hack(常规场景不推荐),如下:

image_2023-03-17_20230317231546


这是因为,在application/x-www-form-urlencoded中,key=value格式,value为空时,可以传key=,也可以省略掉等号传key,所以我们取第一个key值就拿到了请求体数据。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK