最近分析數(shù)據(jù)偶然發(fā)現(xiàn)nginx log中有一批用戶所有的HTTP POST log上報(bào)請(qǐng)求均返回400,沒(méi)有任何200成功記錄,由于只占整體請(qǐng)求的不到0.5%,所以之前也一直沒(méi)有觸發(fā)監(jiān)控報(bào)警,而且很奇怪的是只對(duì)于log上報(bào)的POST接口會(huì)存在這種特定用戶全部400的情況,而對(duì)于其他接口無(wú)論P(yáng)OST還是GET均沒(méi)有此類(lèi)問(wèn)題。
進(jìn)一步分析log發(fā)現(xiàn)其實(shí)對(duì)某些地區(qū)的用戶請(qǐng)求,這個(gè)比例甚至超過(guò)了10%,于是花時(shí)間跟進(jìn)了一下,最終發(fā)現(xiàn)源于部分機(jī)型客戶端發(fā)出的HTTP請(qǐng)求格式不規(guī)范導(dǎo)致,這里記錄一下分析過(guò)程、原因以及最終解決方案。
成都創(chuàng)新互聯(lián)專(zhuān)注于企業(yè)營(yíng)銷(xiāo)型網(wǎng)站建設(shè)、網(wǎng)站重做改版、松桃網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、成都h5網(wǎng)站建設(shè)、購(gòu)物商城網(wǎng)站建設(shè)、集團(tuán)公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)公司、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性?xún)r(jià)比高,為松桃等各大城市提供網(wǎng)站開(kāi)發(fā)制作服務(wù)。
搜尋網(wǎng)上資料,發(fā)現(xiàn)一般可能有以下幾個(gè)原因會(huì)導(dǎo)致nginx響應(yīng)400:
這些錯(cuò)誤其實(shí)都是發(fā)生在nginx這一層,即nginx處理時(shí)認(rèn)為客戶端請(qǐng)求格式錯(cuò)誤,于是直接返回400,不會(huì)向upstream server轉(zhuǎn)發(fā)請(qǐng)求,因而upstream server對(duì)這些錯(cuò)誤請(qǐng)求其實(shí)完全是無(wú)感知的。
而這次根據(jù)nginx log分析,可以看到nginx其實(shí)有向upstream server轉(zhuǎn)發(fā)請(qǐng)求--upstream_addr已經(jīng)是upstream server 有效地址,所以400實(shí)際應(yīng)當(dāng)是upstream server返回的,而不是nginx直接返回,這說(shuō)明至少nginx這一層認(rèn)為請(qǐng)求格式是沒(méi)問(wèn)題的。
截取部分線上部分用戶的錯(cuò)誤日志,其大體樣式如下
127.0.0.1: - 24/Apr/2022:00:50:07 +0900 127.0.0.1:1080 0.000 0.000 POST /log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&device_type=android&osn=Android OS 10 / API-29 (QKQ1..002/V12.0.6.0.QFKCNXM)&channel=Google Play&build=Android OS 10 / API-29 (QKQ1..002/V12.0.6.0.QFKCNXM)&resolution=1080x2340&ts= HTTP/1.1 400 50 - curl/7.52.1 - 0.000 0.000 127.0.0.1 1563 2021
日志分析可以發(fā)現(xiàn)大部分400請(qǐng)求都有一個(gè)問(wèn)題:其query參數(shù)并未經(jīng)過(guò)urlencode,比如可以很明顯看到其參數(shù)channel=Google Play 中的空格并未轉(zhuǎn)碼成%20,直覺(jué)上推斷這應(yīng)該和400的原因有直接關(guān)系。
為了驗(yàn)證未轉(zhuǎn)碼query參數(shù)是否是導(dǎo)致400的直接原因,簡(jiǎn)單通過(guò)curl構(gòu)造幾個(gè)測(cè)試http請(qǐng)求:
# 無(wú)空格
curl -v 'http://127.0.0.1/log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google%20Play' -d @test.json
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google%20Play HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Length: 1563
> Content-Type: application/x-www-form-urlencoded
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 200 OK
< Server: nginx/1.16.1
< Date: Sat, 23 Apr 2022 15:54:53 GMT
< Content-Type: application/json
< Content-Length: 22
< Connection: keep-alive
<
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact
# 有空格
curl -v 'http://127.0.0.1/log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google Play' -d @test.json
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 80 (#0)
> POST /log/report?appd=abc.demo.android&appname=abcdemo&v=1.0&langes=zh-CN&phonetype=android&channel=Google Play HTTP/1.1
> Host: 127.0.0.1
> User-Agent: curl/7.52.1
> Accept: */*
> Content-Length: 1563
> Content-Type: application/x-www-form-urlencoded
> Expect: 100-continue
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 400 Bad Request
< Server: nginx/1.16.1
< Date: Sat, 23 Apr 2022 15:55:14 GMT
< Content-Type: text/plain; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
<
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact
發(fā)現(xiàn)凡是帶空格的請(qǐng)求upstream server均會(huì)直接返回400,這里可以推斷query 參數(shù)未urlencode是400問(wèn)題的直接原因了,但是為什么未轉(zhuǎn)碼會(huì)導(dǎo)致400呢?怎么從HTTP原理上解釋這個(gè)現(xiàn)象?為了找到答案,需要回顧了一下HTTP協(xié)議標(biāo)準(zhǔn)。
HTTP的請(qǐng)求消息格式如下:
如上圖所示,作為一種文本協(xié)議,對(duì)HTTP請(qǐng)求消息中不同部分的區(qū)別、拆分完全是基于空格 、回車(chē)符\r、換行符\n這些字符標(biāo)記進(jìn)行的,對(duì)于第一行的三個(gè)部分請(qǐng)求方法、URL和協(xié)議版本的拆分即是根據(jù)空格進(jìn)行split。
分析查到的400 HTTP請(qǐng)求,可以發(fā)現(xiàn)由于query參數(shù)未urlencode,導(dǎo)致其中會(huì)出現(xiàn)空格,這時(shí)嚴(yán)格來(lái)說(shuō)這個(gè)請(qǐng)求已經(jīng)不符合HTTP規(guī)范了,因?yàn)榇藭r(shí)第一行再根據(jù)空格可以split出超過(guò)3部分,無(wú)法與method、URL、version再一一對(duì)應(yīng),從語(yǔ)義上來(lái)說(shuō)此時(shí)直接返回400是合理處理邏輯。
實(shí)際處理中,面對(duì)這種情況,有的組件能兼容處理--把split的首部和尾部分別作為method與version,而中間剩余部分統(tǒng)一作為URL,比如nginx即兼容了這種不規(guī)范格式,但是很多組件并不能兼容處理這種情況--畢竟這并不符合HTTP規(guī)范,比如charles抓包此種請(qǐng)求會(huì)出錯(cuò)、golang 的net/http庫(kù)、Django的http模塊收到這類(lèi)請(qǐng)求都會(huì)報(bào)400...
負(fù)責(zé)日志上報(bào)的upstream server是golang實(shí)現(xiàn)的logsvc,其使用標(biāo)準(zhǔn)卡庫(kù)net/http處理HTTP請(qǐng)求,進(jìn)一步探究一下該標(biāo)準(zhǔn)庫(kù)是怎么解析HTTP請(qǐng)求的,以確認(rèn)錯(cuò)誤原因。
根據(jù)golang源碼,可以發(fā)現(xiàn)其HTTP請(qǐng)求解析的路徑為 http.ListenAndServe => http.Serve => serve => readRequest.... 其解析HTTP請(qǐng)求頭的邏輯即位于readRequest函數(shù)中。
readRequest部分代碼如下:
// file: net/http/request.go
...
1009 func readRequest(b *bufio.Reader, deleteHostHeader bool) (req *Request, err error) {
1010 tp := newTextprotoReader(b)
1011 req = new(Request)
1012
1013 // First line: GET /index.html HTTP/1.0
1014 var s string
1015 if s, err = tp.ReadLine(); err != nil {
1016 return nil, err
1017 }
1018 defer func() {
1019 putTextprotoReader(tp)
1020 if err == io.EOF {
1021 err = io.ErrUnexpectedEOF
1022 }
1023 }()
1024
1025 var ok bool
1026 req.Method, req.RequestURI, req.Proto, ok = parseRequestLine(s)
1027 if !ok {
1028 return nil, &badStringError{"malformed HTTP request", s}
1029 }
1030 if !validMethod(req.Method) {
1031 return nil, &badStringError{"invalid method", req.Method}
1032 }
1033 rawurl := req.RequestURI
1034 if req.ProtoMajor, req.ProtoMinor, ok = ParseHTTPVersion(req.Proto); !ok {
1035 return nil, &badStringError{"malformed HTTP version", req.Proto}
1036 }
...
可以看到readRequest中先通過(guò)parseRequestLine解析出首行的method, URL與Proto三個(gè)字段,然后通過(guò)ParseHTTPVersion解析version是否正確,不正確則報(bào)錯(cuò){"malformed HTTP version", 最終會(huì)導(dǎo)致響應(yīng)400。
parseRequestLine代碼如下:
...
966 // parseRequestLine parses "GET /foo HTTP/1.1" into its three parts.
967 func parseRequestLine(line string) (method, requestURI, proto string, ok bool) {
968 s1 := strings.Index(line, " ")
969 s2 := strings.Index(line[s1+1:], " ")
970 if s1 < 0 || s2 < 0 {
971 return
972 }
973 s2 += s1 + 1
974 return line[:s1], line[s1+1 : s2], line[s2+1:], true
975 }
可以看到parseRequestLine的解析代碼是通過(guò)查找第0個(gè)、第1個(gè)空格index,然后直接基于slice語(yǔ)法將其切成了method、requestURI、proto三部分,如果requestURI中包含額外空格,會(huì)導(dǎo)致proto取值實(shí)際變?yōu)榈谝粋€(gè)空格之后的所有字符,比如"POST abc/?x=o space d HTTP/1.1"會(huì)被解析為:method=POST, requestURI=abc/?x=0, proto=" space d HTTP/1.1",這會(huì)導(dǎo)致下一步ParseHTTPVersion解析出錯(cuò)。
ParseHTTPVersion代碼如下,可以發(fā)現(xiàn)之前parseRequestLine解析得到的version字段如果不合法,則會(huì)返回錯(cuò)誤:
...
769 // ParseHTTPVersion parses an HTTP version string.
770 // "HTTP/1.0" returns (1, 0, true).
771 func ParseHTTPVersion(vers string) (major, minor int, ok bool) {
772 const Big = // arbitrary upper bound
773 switch vers {
774 case "HTTP/1.1":
775 return 1, 1, true
776 case "HTTP/1.0":
777 return 1, 0, true
778 }
779 if !strings.HasPrefix(vers, "HTTP/") {
780 return 0, 0, false
781 }
782 dot := strings.Index(vers, ".")
783 if dot < 0 {
784 return 0, 0, false
785 }
786 major, err := strconv.Atoi(vers[5:dot])
787 if err != nil || major < 0 || major > Big {
788 return 0, 0, false
789 }
790 minor, err = strconv.Atoi(vers[dot+1:])
791 if err != nil || minor < 0 || minor > Big {
792 return 0, 0, false
793 }
794 return major, minor, true
795 }
首先要做的是先和客戶端對(duì)齊問(wèn)題,客戶端確認(rèn)部分機(jī)型上其調(diào)用unity的網(wǎng)絡(luò)庫(kù)方法未能對(duì)其query參數(shù)正常urlencode,新版本將在unity網(wǎng)絡(luò)庫(kù)之上增加額外代碼保證所有參數(shù)必須urlencode,使其符合HTTP規(guī)范。
而后進(jìn)一步考慮可否先臨時(shí)兼容處理線上已有的異常請(qǐng)求,防止新版本覆蓋修復(fù)前這部分異常用戶log上報(bào)數(shù)據(jù)的持續(xù)丟失,針對(duì)兼容考慮了以下幾個(gè)方案
由于日志服務(wù)由獨(dú)立的golang server負(fù)責(zé),其代碼邏輯很簡(jiǎn)單:只是對(duì)log 的POST請(qǐng)求的body進(jìn)行解壓縮、解析、寫(xiě)入kafka,并無(wú)其他額外邏輯,改動(dòng)成本較低,因此先考慮了替換net/http為其他三方庫(kù)看是否能解決問(wèn)題。
先后嘗試了流行的gin和echo庫(kù)發(fā)現(xiàn)都報(bào)400,忍不住又探究了其源碼,結(jié)果發(fā)現(xiàn)這兩個(gè)庫(kù)內(nèi)部其實(shí)都調(diào)用了net/http 的ListenAndServer 和 Serve方法,其前面的解析邏輯就是net/http對(duì)應(yīng)代碼負(fù)責(zé)的,因而自然也會(huì)報(bào)400。
想到的另一個(gè)可能方法是在nginx層使用lua/perl腳本對(duì)傳入的未urlencode的request_uri參數(shù)進(jìn)行urlencode后再發(fā)給upstream server,但是發(fā)現(xiàn)線上nginx編譯時(shí)并未集成lua、perl的模塊。要采用此種方法則只能:
考慮到本人作為RD而非專(zhuān)業(yè)nginx OP人員,和對(duì)線上影響的風(fēng)險(xiǎn)不輕易嘗試。
開(kāi)頭提到過(guò)對(duì)于待空格的異常請(qǐng)求,只有l(wèi)og上報(bào)POST接口會(huì)返回400,其他接口都返回正常,這其實(shí)是因?yàn)樵趎ginx轉(zhuǎn)發(fā)時(shí)對(duì)正常的業(yè)務(wù)接口和log接口進(jìn)行了拆分,log/report接口會(huì)單獨(dú)轉(zhuǎn)發(fā)到獨(dú)立的golang logsvc服務(wù),而正常業(yè)務(wù)請(qǐng)求均會(huì)轉(zhuǎn)發(fā)給python的主api服務(wù)。
回顧當(dāng)初之所以會(huì)拆分一個(gè)單獨(dú)的golang server負(fù)責(zé)app log上報(bào)的解析和寫(xiě)kafka,而不再和其他接口邏輯一樣都由主api服務(wù)負(fù)責(zé),主要是兩個(gè)原因:
當(dāng)前l(fā)ogsvc無(wú)法處理的此種情況,使用uwsgi協(xié)議與nginx交互的api主服務(wù)卻可以正常解析,因而在nginx添加如下臨時(shí)配置:
location /log/report {
include proxy_params;
if ( $args !~ "^(.*) (.*)$" ) {
proxy_pass http://test_log_stream;
break;
}
include uwsgi_params;
uwsgi_pass test_api_stream;
}
即通過(guò)正則匹配query參數(shù)(args)中若不存在空格直接交由logsvc處理,存在空格則交由使用uwsgi協(xié)議的api主服務(wù)處理,由于此類(lèi)異常請(qǐng)求僅占整體請(qǐng)求的不到0.5%,之前考慮的拆分架構(gòu)依然work,只是對(duì)于少量的異常請(qǐng)求先通過(guò)api主服務(wù)進(jìn)行兼容處理。
轉(zhuǎn)載請(qǐng)注明出處,原文地址: https://www.cnblogs.com/AcAc-t/p/nginx_400_problem_for_not_encode_http_request.html
https://www.cnblogs.com/ranyonsue/p/.html
https://www..com/archives/.html
https://blog.csdn.net/iamlihongwei/article/details/
https://dbaplus.cn/news-21-1129-1.html
https://blog.51cto.com/leejia/
https://blog.csdn.net/kaosini/article/details/
當(dāng)前名稱(chēng):一次不規(guī)范HTTP請(qǐng)求引發(fā)的nginx響應(yīng)400問(wèn)題分析與解決
分享網(wǎng)址:http://chinadenli.net/article12/dsoisdc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站制作、外貿(mào)建站、網(wǎng)站設(shè)計(jì)公司、網(wǎng)站改版、網(wǎng)站策劃、做網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)
網(wǎng)頁(yè)設(shè)計(jì)公司知識(shí)