由x-www-form-urlencoded引發(fā)的接口對接失敗

問題發(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();
}

這個邏輯很簡單,如下:

  1. 先從data參數(shù)中取數(shù)據(jù)弃鸦。
  2. 若沒有再從請求體中拿绞吁。
  3. 然后base64解碼。
  4. 最后轉(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個例子

  1. 如果ContentType是application/x-www-form-urlencoded時爸舒,請求可能是這樣的:
  2. 如果ContentType是application/json時蟋字,請求可能是這樣的:
  3. 如果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)通了鸳劳!

解決問題

解決這個問題很簡單狰贯,如下:

  1. 讓調(diào)用方在請求體里加上data=,以符合application/x-www-form-urlencoded的key=value規(guī)范。
  2. 讓調(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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绘雁,一起剝皮案震驚了整個濱河市橡疼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌庐舟,老刑警劉巖欣除,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異挪略,居然都是意外死亡历帚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門杠娱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抹缕,“玉大人,你說我怎么就攤上這事墨辛。” “怎么了趴俘?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵睹簇,是天一觀的道長。 經(jīng)常有香客問我寥闪,道長太惠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任疲憋,我火速辦了婚禮凿渊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己埃脏,他們只是感情好搪锣,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著彩掐,像睡著了一般构舟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上堵幽,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天狗超,我揣著相機(jī)與錄音,去河邊找鬼朴下。 笑死努咐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的殴胧。 我是一名探鬼主播渗稍,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼溃肪!你這毒婦竟也來了免胃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤惫撰,失蹤者是張志新(化名)和其女友劉穎羔沙,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厨钻,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡扼雏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了夯膀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诗充。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖诱建,靈堂內(nèi)的尸體忽然破棺而出蝴蜓,到底是詐尸還是另有隱情,我是刑警寧澤俺猿,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布茎匠,位于F島的核電站,受9級特大地震影響押袍,放射性物質(zhì)發(fā)生泄漏诵冒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一谊惭、第九天 我趴在偏房一處隱蔽的房頂上張望汽馋。 院中可真熱鬧侮东,春花似錦、人聲如沸豹芯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽告组。三九已至煤伟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間木缝,已是汗流浹背便锨。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留我碟,地道東北人放案。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像矫俺,于是被迫代替她去往敵國和親吱殉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容