工作雜記:ASP.NET Core 5 MVC 解決檔案上傳損壞問題

工作雜記:ASP.NET Core 5 MVC 解決檔案上傳損壞問題

問題描述

昨天收到朋友的求救,他在其他駐點的工程師遇到一個奇怪的問題,已經處理三、四天還是解決不了,請我幫忙處理。

對方負責的專案是用 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
    5
    public sealed class InputModel
    {
    public IFormFile FormFile { get; set; }
    // 略
    }
  • Controller 原始碼

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    [HttpPost]
    [Route("UploadExample")]
    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 原始碼

Startup.cs

在 pipeline 註冊 Middleware 的地方果然看到可疑分子,在發佈的版本會用到 ExceptionMiddleware 這個 Middleware,再請對方提供 ExceptionMiddleware 原始碼給我看。

  • ExceptionMiddleware.cs 原始碼

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 測試,問題解決了。

總結

這個問題難解的原因是他隱藏在第一眼看不到的地方,就像病人說他腳痛,但腳其實很健康,真正的病因是腦炎,如果只醫腳,病永遠不會好。

之前上一堆課程、看許多文章和書果然沒白費,累積的知識點都用上了,如果缺少這些知識點,我就不具備在短時間內可找到這個問題的能力,也決定把這段修問題的過程分享出來,希望將來有天也能幫到其他人。