從Bug重新複習POST常見的提交數據格式

從Bug重新複習POST常見的提交數據格式

從Bug重新複習POST常見的提交數據方式

從Bug重新複習POST常見的提交數據方式, 最近在做專案時發生了一個問題:當我用axios.post資料到後端時,回傳的狀態是200無問題,但是就是內文出現了警告的內容:

<br />
<b>Deprecated</b>:  Automatically populating $HTTP_RAW_POST_DATA is deprecated and will be removed in a future version. To avoid this warning set 'always_populate_raw_post_data' to '-1' in php.ini and use the php://input stream instead. in <b>Unknown</b> on line <b>0</b><br />
<br />
<b>Warning</b>:  Cannot modify header information - headers already sent in <b>Unknown</b> on line <b>0</b><br />

上網查了很多資料,最後的最後發現竟然是PHP本身的bug。這段bug本身的大意是說你有呼叫到了$HTTP_RAW_POST_DATA這個變數,但是這個變數在這個PHP版本(筆者是使用PHP 5.6)是不建議使用的,之後這個變數也會被 remove。如果你不想要看到這個錯誤資訊可以去php.ini把always_populate_raw_post_data 設為-1, 並且運用php://input串流的方式取資料。不過根據網路上的說法,似乎把always_populate_raw_post_data修改成-1也沒有作用,因為他本身就是個Bug…

至於為什麼會出現這個bug?為什麼會用到$HTTP_RAW_POST_DATA這個變數?這邊就要先來談談POST的常見的提交數據的方式。

淺談HTTP協定及POST

根據HTTP/1.1協議規定,每一次的HTTP傳輸都會包含請求(Request)及回應(Response),而HTTP協定本身由多個部分組成:

  • Request-URL
  • method(verb)
  • headers(status)
  • entity-body

而POST是方法(method)的其中一種,一般用來向Server端提交資訊,根據協議的規定,POST提交的資訊必須放在entity-body 內,但是協議內並無規定要用哪種編碼方式,所以開發者可以自己決定編碼方式,只要符合上述的HTTP協議格式即可。

但是當數據發送出去後,Server端要可以解析,整個請求才會是有意義。一般的Server程式語言如PHP, Python都具備此功能。而Server端一般是根據請求的標頭(Header)中的Content-Type內的訊息得知內文是由何種方式編碼,再對主體(entity-body)進行解析。我們在發送API請求給後端時,第一個要確認的是發送的Content-Type的型態,所以我們要先知道Content-Type的常見型態。

application/x-www-form-urlencoded

這應該是最常見的POST提交數據的方式,透過瀏覽器的<form>標籤產生,沒有加上enctype屬性,會用application/x-www-form-urlencoded方式傳遞(若加上enctype時則為multipart/form-data),提交的內容會以key-value方式配對,而在後端程式如PHP,可以由$_POST[‘key’]的方式取值。而有些第三方套件再提交數據時預設也是用此方式,如Jquery。

若你是使用Chrome瀏覽器,打開網頁後點擊右鍵進入檢查功能。選擇Network後選取一個網頁請求後可以看到相關資訊,如Request Header

>Request Header
Accept:application/json, text/plain, */*
Accept-Encoding:gzip, deflate, br
Accept-Language:zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6
Connection:keep-alive
Content-Length:12
Content-Type:application/x-www-form-urlencoded;charset=UTF-8

及實體內文內容,是一個key-value配對

> Form Data
user: Dustin
password: 123
...
multipart/form-data

multipart/form-dataapplication/x-www-form-urlencoded很像,都是很常見的提交方式,差異只在若發送表單時<form>有設置enctype時可以傳遞文件檔案,而Content-type會以multipart/form-data顯示。

但是他的請求內容較application/x-www-form-urlencoded複雜,以下是我從Postman發送請求後的數據:

POST YOUR/POST/URL HTTP/1.1
Host: YOURHOST.com
Cache-Control: no-cache
Postman-Token: xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; user="dustin"

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; password="123"

------WebKitFormBoundary7MA4YWxkTrZu0gW--

可以看到Content-Typemultipart/form-data,而比較特別的是生成了一個boundary用來分割字段,在entity-body內按參數分為多個結構部分,每個部分以boundary作為開頭,緊接著是內容描述訊息(Content-Disposition: form-data),最後是傳遞資料內容 (user=”dustin”),而最後再以boundary結束。

application/json

若是Content-Type 為 application/json,傳遞的訊息是以JSON(Javascript Orvect Notation)為主,目前越來越多的服務都是用 json當做資料來傳遞。而除了較低版本的IE瀏覽器外,都原生支持JSON.stringigy,一個用來形成 json的JS 函式,且 Server端的語言也大都支持json的解析,如PHP的json_decode

當使用application/json 當做Content-Type時,請求的主體會變成Request payload,而內容也會是單純字串。所以在Server 端在處理請求時就需要依照json的方式處理,如PHP的話會比較複雜,因為無法從$_POST[‘key’]的方式取得(因為無法辨識key),所以一般的方式會是用'php://input'取得串流資料再透過json_decode解析。

$array = json_decode(file_get_contents('php://input'), true)
//json_decode預設為object, 若第二參數傳入true為array

最後發送的請求會是

>Request payload
{"user":"dustin", "password":"123"}

在Postman內指的是raw 內的資料,就是單純輸入文本(text),因為 json本身僅是一個字串,只是用類似 Javascript 物件的表示方式顯示。

其他

目前筆者常遇到的Content-Type型態為上述三者,而當然也還有其他的型態,如text/xml, binary等,但是筆者不熟悉,所以這邊就不著墨介紹。

Bug解析

解釋完了常見的Content-Type型態後,回到題目本身—這個bug:$HTTP_RAW_POST_DATA到底從哪來?

原因就在於當你使用 axios.post的時候,預設並不是application/x-www-form-urlencoded的形式!而是以application/json的形式傳遞,所以本體(entity body)內的資料就會是字串,也就是上面提到的Request payload或是稱為raw data。而在PHP版本為5.6時,預設情況下是不啟用$HTTP_RAW_POST_DATA的(不過因為他本身就是個bug,所以就算改了也沒用…),所以你無法辨識出payload的資料,不過若您是使用Laravel作為後端開發,Laravel會幫你解析掉這個部分,但是還是會因為php版本的問題回覆錯誤資訊。以這個錯誤資訊其實不會對程式運作造成太大影響,報錯的部份無法刪除也僅是PHP造成的bug

但是若您也像我一樣,看到了覺得礙眼,其實還是有方法解決–就是把提交資訊的方式改成用application/x-www-form-urlencoded的方式就一勞永逸啦!既不會碰觸到raw data的問題,也可以讓提交資訊的方式改用form這種常見的方式。所以我們就從axios下手總可以了吧!但是事情總不是憨人想的那麼的簡單… 沒想到遇到另一個bug!!

Axios.post的Content-Type bug

正當我們上網尋找更改Axios的Content-Type的方法時,有兩種方式,一種是在預設的時候修改,例如在axios初始化時處理。

axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'

或是你在每個請求時處理

axios({
    method: 'post',
    url: YOUR_URL,
    data: data,
    headers: {
        'Content-type': 'application/x-www-form-urlencoded'
    }
});

這樣總行了吧…咦!但是還是會發現傳送的資料型態還是application/json,根本沒變成application/x-www-form-urlencoded阿!後來才發現,這又是另一個bug…,這個bug從2016年初就提出,沒想到到今年(2018/1)都還沒被解決阿!。最後的最後在討論串的最下面有人回覆官方有提供解套方法,就是調用URLSearchParams API,然後把值傳入即可,不過需要注意有瀏覽器版本支援的問題,以及建議可以用polyfill解決調版本問題。

所以傳遞資料的方式會改成:

let params = new URLSearchParams();
params.append('user', 'dustin');
params.append('password', '123');
axios.post(url, params).then().catch()

最後終於發現傳遞資料的方式變成:

> Request Headers
Content-Type:application/x-www-form-urlencoded;charset=UTF-8;

>Form data
user:dustin
password:123

最後再檢查一下Response,終於是呈現空白而不是之前顯示的錯誤訊息了…

後記

沒想到只是處理一個axios.post 的問題竟然碰到兩個套件/語言本身的bug,讓這套踩坑之旅真是加倍艱辛阿!不過也因為這個原因又去複習了一下網路協定的相關規則,希望可以讓自己的觀念更加踏實。

參考資料:

Jerry Qu : 四种常见的 POST 提交数据方式

axios