問題發(fā)生
這周正在寫代碼,突然抬吟,旁邊小哥問我個問題...
- 小哥:我這有個接口,自己調(diào)用沒有問題,但別人調(diào)用就不行宋下,這種問題該如何排查缺亮?
- 我:抓下包看看呢...
- 小哥:是這樣使用tcpdump嗎蜗字?
- 我:是的
待小哥抓到包后盗迟,使用wireshark打開,并找到了相應(yīng)的請求,類似如下:
然后我讓小哥將這個請求伞插,使用curl發(fā)一個同樣的請求割粮,看能不能復(fù)現(xiàn)這個錯誤,如下:
$ curl -X POST localhost:80/api \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'eyJvcmRlcl9pZCI6MTIzNDU2Nzg5MDIxNDN9Cg=='
命令執(zhí)行之后媚污,重現(xiàn)了調(diào)用方一樣的接口報錯舀瓢。
然后抓包小哥自己的正確請求是這樣的:
這里很容易發(fā)現(xiàn),別人調(diào)不通接口耗美,小哥能調(diào)通京髓,原因是別人的請求體里面缺失
data=
這一段??
先不管為什么缺這個會報錯,這里展示了一個實用技巧商架,對于http接口來說堰怨,排查這種接口調(diào)用差異問題,最直接高效的方法蛇摸,就是對比正確調(diào)用與錯誤調(diào)用的數(shù)據(jù)包备图!
問題解決
那么接下來,就是研究為什么報錯了赶袄,看看服務(wù)端的處理代碼揽涮,大概如下:
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();
}
這個邏輯很簡單,如下:
- 先從data參數(shù)中取數(shù)據(jù)弃鸦。
- 若沒有再從請求體中拿绞吁。
- 然后base64解碼。
- 最后轉(zhuǎn)json對象唬格。
我們接口基本都這樣,使用base64將數(shù)據(jù)包了一層颜说,許多年過去了购岗,具體原因不詳,不深究??
從上面處理邏輯看门粪,按道理小哥的調(diào)用方式與別人的調(diào)用方式都是支持的喊积,理論上來說,小哥的調(diào)用方式會命中request.getParameter
玄妈,而別人的調(diào)用方式會命中request.getInputStream()
乾吻,那為啥別人的調(diào)用方式不行?
小哥又調(diào)試了下上述服務(wù)端代碼拟蜻,發(fā)現(xiàn)使用別人的調(diào)用方式時绎签,從request.getInputStream()
中讀不到數(shù)據(jù)??
我在小哥旁邊,提示將ContentType改成text/plain
試試酝锅,curl命令改成這樣:
$ curl -X POST localhost:80/api \
-H 'Content-Type: text/plain' \
-d 'eyJvcmRlcl9pZCI6MTIzNDU2Nzg5MDIxNDN9Cg=='
執(zhí)行這條命令后诡必,接口返回了正確結(jié)果??
那為什么會這樣呢??????
ContentType指的是什么?
首先來看看ContentType指的是什么搔扁,看2個例子
- 如果ContentType是
application/x-www-form-urlencoded
時爸舒,請求可能是這樣的:
- 如果ContentType是
application/json
時蟋字,請求可能是這樣的:
- 如果ContentType是
application/xml
時,請求可能是這樣的:
不難發(fā)現(xiàn)扭勉,ContentType這個請求頭的作用是鹊奖,指定請求體的數(shù)據(jù)格式。比如application/x-www-form-urlencoded
表示請求體是key=value格式涂炎,application/json
表示請求體是json格式忠聚,application/xml
表示是xml格式,而text/plain
表示請求體是純文本璧尸。
那為什么將ContentType從application/x-www-form-urlencoded
變成text/plain
咒林,報錯的調(diào)用就能跑通了?
application/x-www-form-urlencoded有何不同爷光?
application/x-www-form-urlencoded
是個歷史非常悠久的ContentType了垫竞,它通過key=value的形式來組織表單數(shù)據(jù),當(dāng)然key和value還需要做urlencode編碼蛀序。
而正是因為它如此悠久欢瞪,所以被采納在了web服務(wù)器的實現(xiàn)標(biāo)準(zhǔn)中,幾乎所有的web服務(wù)器徐裸,當(dāng)發(fā)現(xiàn)ContentType是application/x-www-form-urlencoded
時遣鼓,會自動按key=value&key2=value2的格式來解析請求體數(shù)據(jù),解析完成后重贺,我們就可以通過request.getParameter()
來獲取對應(yīng)key的值了骑祟。
比如Tomcat的實現(xiàn)在org.apache.catalina.connector.Request#parseParameters,如下:
解析key=value格式數(shù)據(jù)如下:
但是气笙,這里有一個重要的細(xì)節(jié)次企!
當(dāng)ContentType是application/x-www-form-urlencoded
時,由于Tomcat提前將請求體的數(shù)據(jù)流讀了一遍潜圃,所以后面再通過request.getInputStream()
就讀不到請求體數(shù)據(jù)了缸棵。
如下,從request.getInputStream()
中獲取到的流谭期,pos游標(biāo)已經(jīng)走到了lim結(jié)束位置了堵第。
而將ContentType改為text/plain
后,Tomcat不會解析請求體隧出,所以就不會讀數(shù)據(jù)流踏志,自然后面我們通過request.getInputStream()
就又能讀到數(shù)據(jù)了,故又可以調(diào)通了鸳劳!
解決問題
解決這個問題很簡單狰贯,如下:
- 讓調(diào)用方在請求體里加上
data=
,以符合application/x-www-form-urlencoded
的key=value規(guī)范。 - 讓調(diào)用方將ContentType修改為
text/plain
涵紊,因為調(diào)用方的請求數(shù)據(jù)就是base64純文本而已傍妒,我們讓調(diào)用方選擇了這個方案。
如果調(diào)用方有很多摸柄,難以確定調(diào)用方的規(guī)范情況颤练,那其實還有一種方案,通過request.getParameterMap()
實現(xiàn)驱负,代碼有點hack(常規(guī)場景不推薦)嗦玖,如下:
這是因為,在
application/x-www-form-urlencoded
中跃脊,key=value
格式宇挫,value為空時,可以傳key=
酪术,也可以省略掉等號傳key
器瘪,所以我們?nèi)〉谝粋€key值就拿到了請求體數(shù)據(jù)。