摘要:於是確認問題的原因是 WebClient 在處理響應的時候沒有根據 Content-Encoding 的值解壓縮下載下來的文件。這幾乎可以肯定,問題出在 .NET 的 WebClient 上,可能是請求不對,或者對響應的後續處理不對。

一直在使用 WebClient 下載文件,.NET 已經封裝好,所以用起來代碼非常簡潔;但直到今天發現有一個文件一直不能正確下載下來。

本文介紹這個問題的原因和解決方法,更重要的是給出調查方法。

本文所涉及到的域名已經過敏感信息處理,所以實際上你是無法訪問到的;但這不影響本文對調查方法的描述。

問題

我原本是使用如下的代碼去下載任意文件的(參數經過簡化)。

private static async Task DownloadFileAsync()
{
    var url = "http://localhost:5000/walterlv-icon.svg";
    var fileName = @"C:\Users\lvyi\Desktop\TEST\walterlv-icon.svg";

    using var webClient = new WebClient();
    webClient.DownloadFile(new Uri(url), fileName);
}

現在,下載一個 svg 的時候,原本應該是如下的圖片:

然而實際上下載下來之後卻是這樣的:

原本大小是 992 字節,實際下載下來後是 508 字節,而且固定是 508 字節。你可以通過右鍵複製圖片地址,然後分別把兩張圖下載下來看。

調查

顯然, WebClient 沒有拋出任何異常,而且每次下載下來都是固定的 508 字節,說明肯定不是網絡不通或程序提前退出導致的,也不是線程安全相關的問題。基本可以認定爲問題出在服務器的配置,或者客戶端的請求上。

使用其他“正常”下載器嘗試

拿 Chrome 跑以上地址,拿專用下載工具跑以上地址,甚至是拿 Postman 跑以上地址,都可以成功顯示或者下載到正確的圖片。

這幾乎可以肯定,問題出在 .NET 的 WebClient 上,可能是請求不對,或者對響應的後續處理不對。

使用 Postman 和 WebClient 對比測試

爲了對比請求和響應,我使用的是 Fiddler 抓包。

WebClient 請求:

GET http://localhost:5000/walterlv-icon.svg HTTP/1.1
Host: localhost:5000
Connection: Keep-Alive

WebClient 響應:

HTTP/1.1 200 OK
Date: Tue, 03 Mar 2020 07:54:53 GMT
Content-Type: image/svg+xml
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Tue, 02 Mar 2021 09:16:30 GMT
Server: JSP3/2.0.14
Content-Encoding: gzip
ETag: W/"Fid8uZXkFIrQWqDgx84YL_8PPqIi"
Last-Modified: Wed, 08 Jan 2020 04:20:14 GMT
Accept-Ranges: bytes
Cache-Control: public, max-age=31536000
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: X-Log, X-Reqid
Access-Control-Max-Age: 2592000
Content-Disposition: inline; filename="walterlv-icon.svg"; filename*=utf-8''walterlv-icon.svg
Content-MD5: +6J/pVyMK4cWQf8n3gMnbQ==
Content-Transfer-Encoding: binary
X-Log: X-Log
X-M-Log: QNM:xs441;SRCPROXY:xs1756;SRC:39;SRCPROXY:39;QNM3:40
X-M-Reqid: wgIAAAiVtix6zucV
X-Qiniu-Zone: 0
X-Qnm-Cache: Miss
X-Reqid: ttYAAAAPpS16zucV
X-Svr: IO
Ohc-File-Size: 992
Timing-Allow-Origin: *
Ohc-Cache-HIT: suz2ct85 [3], njctcache85 [1], qdix85 [1]
Age: 81503
X-Via: 1.1 PShbhgdx4pn187:2 (Cdn Cache Server V2.0)[0 200 0], 1.1 PS-FOC-01tho70:12 (Cdn Cache Server V2.0)[0 200 0]
Access-Control-Allow-Methods: GET,OPTIONS
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: *

1fc
        S]o 0}Τ  ;4  1 
V-  & k u   4 ɯ m i a  9  k_  aW ! d 61b " *m  n <FM  ? } K O?o@   }u e
 !  _ <% 

*** FIDDLER: RawDisplay truncated at 128 characters. Right-click to disable truncation. ***

Postman 請求:

GET http://localhost:5000/walterlv-icon.svg HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.22.0
Accept: */*
Cache-Control: no-cache
Postman-Token: 05bb3d80-d7a7-4c0d-bdd1-9cd65d79ecab
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Postman 響應:

HTTP/1.1 200 OK
Date: Tue, 03 Mar 2020 07:58:26 GMT
Content-Type: image/svg+xml
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Tue, 02 Mar 2021 09:16:30 GMT
Server: JSP3/2.0.14
Content-Encoding: gzip
ETag: W/"Fid8uZXkFIrQWqDgx84YL_8PPqIi"
Last-Modified: Wed, 08 Jan 2020 04:20:14 GMT
Accept-Ranges: bytes
Cache-Control: public, max-age=31536000
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: X-Log, X-Reqid
Access-Control-Max-Age: 2592000
Content-Disposition: inline; filename="walterlv-icon.svg"; filename*=utf-8''walterlv-icon.svg
Content-MD5: +6J/pVyMK4cWQf8n3gMnbQ==
Content-Transfer-Encoding: binary
X-Log: X-Log
X-M-Log: QNM:xs441;SRCPROXY:xs1756;SRC:39;SRCPROXY:39;QNM3:40
X-M-Reqid: wgIAAAiVtix6zucV
X-Qiniu-Zone: 0
X-Qnm-Cache: Miss
X-Reqid: ttYAAAAPpS16zucV
X-Svr: IO
Ohc-File-Size: 992
Timing-Allow-Origin: *
Ohc-Cache-HIT: suz2ct85 [3], njctcache85 [1], qdix85 [1]
Age: 81716
X-Via: 1.1 PShbhgdx4pn187:2 (Cdn Cache Server V2.0)[0 200 0], 1.1 PS-FOC-01tho70:12 (Cdn Cache Server V2.0)[0 200 0]
Access-Control-Allow-Methods: GET,OPTIONS
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: *

1fc
        S]o 0}Τ  ;4  1 
V-  & k u   4 ɯ m i a  9  k_  aW ! d 61b " *m  n <FM  ? } K O?o@   }u e
 !  _ <% 

*** FIDDLER: RawDisplay truncated at 128 characters. Right-click to disable truncation. ***

請求和響應貼得很長,這可以讓比較感興趣的小夥伴仔細比較。但這裏我直接給出我比較後的結論:

  1. Postman 的請求會發送比較多的頭
  2. 兩者的響應幾乎相同(包括文件大小和內容)

由於響應幾乎相同,所以實際上前面請求頭的不同可以忽略了(至少說明返回的內容沒有因爲請求的不同而有所變化),我們能夠拿到完整的整個文件。

那麼問題基本確定就是在 WebClient 對這個響應的處理上了。

可以注意到 Postman 的請求中有 Accept-Encoding ,兩折的響應中都有 Content-Encoding ,指定了 gzip 。然而這是 Linux 中用來壓縮文件的命令。響應中指定了內容編碼方式爲 gzip 是否意味着我們下載下來的文件實際上是一個 gzip 壓縮文件呢?

於是我將下載下來的文件擴展名改爲 gzip,用壓縮文件打開,於是真的可以解壓出來真實的圖片。

於是確認問題的原因是 WebClient 在處理響應的時候沒有根據 Content-Encoding 的值解壓縮下載下來的文件。

解決

解決的思路:

  • 使 WebClient 支持下載文件後解壓縮

使 WebClient 支持下載文件後解壓縮

各種檢查後發現, WebClient 竟然沒有提供設置解壓縮相關的屬性。慶幸的是,在網上搜索 WebClientgzip 關鍵字後,找到了這一篇答案: .net - Automatically decompress gzip response via WebClient.DownloadData - Stack Overflow

我們需要重寫 WebClient.GetWebRequest 方法,然後改寫 AutomaticDecompression 屬性。此屬性可以改成 gzipdeflatebr 或者它們的組合,這與 Postman 發請求時聲明支持的值是完全一樣的。

class AutoDecompressionWebClient : WebClient
{
    protected override WebRequest GetWebRequest(Uri address)
    {
        var baseRequest = base.GetWebRequest(address);
        if (baseRequest is HttpWebRequest httpWebRequest)
        {
            httpWebRequest.AutomaticDecompression = DecompressionMethods.All;
        }
        return baseRequest;
    }
}

另外,也可以在拉取到響應的流後自己去做解壓,可以參見:

參考資料

相關文章