問題描述
昨天收到朋友的求救,他在其他駐點的工程師遇到一個奇怪的問題,已經處理三、四天還是解決不了,請我幫忙處理。
對方負責的專案是用 ASP.NET Core 5 MVC 開發的,有一個上傳 Excel 匯入資料的功能,在 IDE 將站台執行起來,可正常上傳並匯入資料,但發佈到測試機的 IIS 站台,檔案上傳完在匯入資料時會發生例外。
為了修正問題,在匯入 Excel 的部份,他嘗試換過各種套件,但全都發生例外,各套件出現的訊息如下:
- ClosedXML:FileFormatException: File contains corrupted data
- NPOI:ZipException: EOF in header
- ExcelDataReader:Offset to Central Directory cannot be held in an Int64
後來他發現上傳到 Server 的 Excel 檔案都無法開啟,似乎檔案都壞了。
也嘗試修改檔案上傳的寫法,但都無效,寫入的檔案就是會損壞。
處理過程
初步猜測
「檔案上傳、本機 IDE 執行正常、發佈到測試機 IIS 異常」從這幾個關鍵點,一開始我在猜會不會和寫入權限有關,但既然檔案都寫進去了,只是檔案壞掉,那就排除權限問題。
接下來是懷疑檔案上傳時,可能遇到 UTF8 編碼相關問題,造成寫出壞檔的結果,先跟對方拿原始碼來看看。
原始碼
以下是上傳檔案相關的程式碼,內容已簡化修改,只保留講解必要的部份。
View (HTML 部份) 原始碼
1
2<input type="file" id="file_Choose" width="400">
<button id="btn_FileUpload">確認上傳</button>View (JS 部份) 原始碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//確定上傳
$('#btn_FileUpload').click(function () {
if (confirmVerify()) {
var file = $('#file_Choose')[0].files[0];
var inputData = new FormData();
inputData.append('FormFile', file);
Upload(inputData);
}
});
//上傳
function Upload(data) {
$.ajax({
url: 'UploadExample',
data: data,
type: 'POST',
dataType: 'json',
contentType: false,
processData: false,
async: true,
// 略
});
}View Model 原始碼
1
2
3
4
5public sealed class InputModel
{
public IFormFile FormFile { get; set; }
// 略
}Controller 原始碼
1
2
3
4
5
6
7
8
9
10
11
12
13[ ]
[ ]
public async Task<IActionResult> UploadExampleAsync([FromForm] InputModel input)
{
// 略
using (var fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite))
{
// 儲存檔案
await input.FormFile.CopyToAsync(fs);
// 略
}
// 略
}
縮小問題範圍
上面的 Code 看起來很正常,頁面是用 AJAX Post 檔案到 Controller,Controller 再用 FileStream 將檔案寫入硬碟,看不出有哪邊會讓檔案壞掉的地方,也沒看到檔案有經過編碼處理的問題。
且對方說在 IDE 執行是正常的,那就先排除這邊 Code 的問題。
如果像無頭蒼蠅亂猜,對找問題很沒效率,所以我先將問題區分為「內在因素」和「外在因素」兩個方向,並試著排除掉一個方向,把問題的範圍縮小。
當下的猜測如下
- 「內在因素」:問題可能是在 Debug 模式和發佈版本之間的差異。
- 「外在因素」:或許連線到測試主機有經過一些網通設備,在上傳檔案時,封包被這些設備加料,導致寫檔損壞。
先從「內在因素」開始,請對方將發佈給測試主機的打包,放到本機 IIS 裡執行,看看是否可以重現出相同問題。
對方測試後,果然在本機 IIS 重現問題了,這時鬆了一口氣,到這邊幾乎可以肯定問題是出在 Debug 和發佈版之間的差異處了,通常這個差異就在 Pipeline 註冊 Middleware 的地方,範圍瞬間縮小許多。
確認問題點
接下來請對方提供 Program.cs、Startup.cs、Appsettings.json 等檔案。
- Startup.cs 原始碼
在 pipeline 註冊 Middleware 的地方果然看到可疑分子,在發佈的版本會用到 ExceptionMiddleware 這個 Middleware,再請對方提供 ExceptionMiddleware 原始碼給我看。
- ExceptionMiddleware.cs 原始碼
啊哈!抓到鬼了,這個 Middleware 把所有傳入的 HttpContext.Request.Body 都做 UFT8 編碼加料了,難怪上傳的檔案在寫入時會損壞,果然和一開始的預測一樣,只是當時沒料到問題會藏在 Middleware 這裡。
請對方先將這段轉換註解掉測試看看,檔案上傳功能正常了,確定就是這裡造成檔案損壞。
加個圖解,讓大家比較容易理解問題點真正的位置。
原圖引用出處:[ASP.NET Core MVC Pipeline] Middleware Pipeline
內文也有解說 Pipeline 實作的細節。
原本對方以為問題是出在 Controller 在處理上傳檔案和匯入 Excel 的方法(圖片 ❶ 處)。
經過抽絲剝繭,真正的問題卻是發生在 Pipeline 的 Middleware(圖片 ❷ 處)。
難怪對方花了三、四天都修不好,因為一直沒有修到問題的源頭。
解決問題
找到問題的源頭了,接下來要思考怎麼解決問題。
會有這段轉換 Request Body 的 Middleware 一定有原因,而且他會影響所有傳入的請求內容,如果拔掉肯定會產生更多問題,但留著又影響檔案上傳的功能。
對方也是剛接手這個案子,不了解這段轉換存在的目的,最後我們決定用比較安全的修改方式,在這段程式碼外再包一層判斷,當 Content-Type 是 multipart/form-data 時,就跳過,不要跑進這一段,這樣就不會影響檔案上傳的功能了。
完成修改,重新發佈到測試機 IIS 測試,問題解決了。
總結
這個問題難解的原因是他隱藏在第一眼看不到的地方,就像病人說他腳痛,但腳其實很健康,真正的病因是腦炎,如果只醫腳,病永遠不會好。
之前上一堆課程、看許多文章和書果然沒白費,累積的知識點都用上了,如果缺少這些知識點,我就不具備在短時間內可找到這個問題的能力,也決定把這段修問題的過程分享出來,希望將來有天也能幫到其他人。