HTTP協定之Expect爬坑

2022-07-20 21:01:14

前言

今天,在對接一個第三方平臺開放介面時遇到一個很棘手的問題,根據介面檔案組裝好報文,使用HttpClient發起POST請求時一直超時,對方伺服器一直不給任何響應。

發起請求的程式碼如下:

using (var httpClient = new HttpClient())
{
    var msg = new HttpRequestMessage()
    {
        Content = new StringContent(postJson, Encoding.UTF8, "application/json"),
        Method = HttpMethod.Post,
        RequestUri = new Uri(apiUrl),
    };
    
    // 這裡會一直阻塞,直到超時
    var res =  httpClient.SendAsync(msg).ConfigureAwait(false).GetAwaiter().GetResult();

    if (res.StatusCode != HttpStatusCode.OK)
    {
        throw new Exception(res.StatusCode.ToString());
    }

    return res.Content.ReadAsStringAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}

非同步請求超時取消錯誤如下:

這種情況首先懷疑對方服務是不是有問題
然而經過確認,對方服務沒問題,並且使用將請求的url報文貼上到PostMan進行請求,迅速得到返回報文,一切正常。

排除了對方服務的問題,那是我們的程式碼問題?
可是上面HttpClient發起Post請求的程式碼寫了不知道多少遍,一直都沒問題,今天怎麼就不行了呢,我敢保證這麼寫沒毛病。

遇到這種情況該如何解決呢?

爬坑過程

遇到這種問題,相比大部分人開始各種引數換來換去,各種庫換來換去,可能最終蒙成了。但是這裡我相信PostMan可以請求成功,強大的HttpClient一定可以,一定是是哪個引數問題,有經驗的老手首先就會想到: 介面的協定中是不是對Header有什麼特別的要求,這裡查詢檔案,沒有什麼特別要求。

控制變數法

既然我們不知道為什麼,也猜不到,那就控制變數法去解決。這裡能想到的就是抓包,抓取PostMan成功的請求報文以及我們失敗的報文,對比差異。

抓包工具使用的是Fiddler

Postman報文

POST http://xxx.xxx.xxx.xxx:30000/parking/carin/V1 HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.29.2
Accept: */*
Postman-Token: 14547b64-d8f6-4b0b-9fa9-48c9ec74a8f6
Host: xxx.xxx.xxx.xxx:30000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 563

{"data": ...這裡省略了具體json內容}

HttpClient報文

POST http://118.31.110.35:30000/parking/carin/V1 HTTP/1.1
Content-Type: application/json; charset=utf-8
Host: 118.31.110.35:30000
Content-Length: 563
Expect: 100-continue
Connection: Keep-Alive

{"data": ...這裡省略了具體json內容}

差異排查

  1. 因為body中的內容是一樣的,這裡就不用對比了。
  2. 兩個請求的Header存在差異,那我們就將差異一個一個抹平。
  3. Content-TypeHttpClient中多了charset=utf-8,這個應該不影響,http協定預設就是utf8。
  4. User-AgentHttpClient中沒有,那我們加上一模一樣的User-Agent,測試,依舊超時。
  5. AcceptHttpClient中沒有,抹平,測試,依舊超時。
  6. Postman-TokenHttpClient中沒有,抹平,測試,依舊超時。
  7. Accept-EncodingHttpClient中沒有,抹平,測試,依舊超時。

到這裡Postman中有的,我們HttpClient中都有了,竟然還超時,這裡雖然已經保證大部分引數都一樣了,但是控制變數法要求所有引數都一樣,這裡還沒有保證,因為HttpClient多了一個Expect頭,我們還沒保證一致。

  1. HttpClient的請求頭中Expect: 100-continuePostman報文中不存在,去掉Expect,測試,成功了!!
  2. 那我們鎖定Expect: 100-continue導致了我們的請求無響應,還原之前所有的抹平操作,僅僅移除Expect: 100-continue,測試,依然成功。

本文為Gui.H原創文章,釋出於公眾號:dotnet之美,轉載註明出處

部落格園首發:https://www.cnblogs.com/springhgui/p/16499439.html

最終解決前言中的問題,僅僅需要新增一行程式碼

msg.Headers.ExpectContinue = false;

ExpectContinues屬性檔案:

至此問題解決,控制變數yyds

Expect是什麼

參考Expect的定義
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect

Expect 是一個請求訊息頭,包含一個期望條件,表示伺服器只有在滿足此期望條件的情況下才能妥善地處理請求。

Expect

規範中只規定了一個期望條件,即 Expect: 100-continue, 對此伺服器可以做出如下回應:

  • 100 如果訊息頭中的期望條件可以得到滿足,使得請求可以順利進行的話,
  • 417 (Expectation Failed) 如果伺服器不能滿足期望條件的話;也可以是其他任意表示使用者端錯誤的狀態碼(4xx)。

例如,如果請求中 Content-Length 的值太大的話,可能會遭到伺服器的拒絕。

Expect有啥好處

讓使用者端在傳送請求資料之前去判斷伺服器是否願意接收該資料,如果伺服器願意接收,使用者端才會真正傳送資料,如果使用者端直接傳送請求資料,但是伺服器又將該請求拒絕的話,這種行為將帶來很大的資源開銷。

Expect有啥坑

不是所有的伺服器都會正確應答100-continue, 比如lighttpd, 就會返回417 Expectation Failed。

超時的原因

HttpClient預設攜帶了Expect頭,我們請求帶上了Expect: 100-continue的話是不會立刻傳送body中的報文給伺服器,需要伺服器需要對Expect: 100-continue做出響應,然而對方伺服器不支援Expect當然不能做出響應,在前言說的問題中,也就是HttpClient在等對方伺服器響應Expect,然後再傳送報文,而對方伺服器看來,我們怎麼還不傳送報文過來,雙方都在等資料,最終HttpClient超時~

以上純屬個人理解,有不正確之處,還請指正~