從檔案下載視角來理解Web API

無風聽海發表於2022-03-28

一、問題源起

從Web From過來的人應該會比較熟悉以下下載檔案的程式碼;

[HttpPost]
[Route("Download")]
public void Download()
{
    HttpResponse response = HttpContext.Current.Response;
    response.Clear();
    response.BufferOutput = true;
    response.AddHeader("Content-Type", "application/octet-stream");            
    response.AddHeader("Content-Disposition", "attachment;filename=myfile.txt");
    GetFileContent().CopyTo(response.OutputStream);
    response.Flush();
}

程式碼中直接修改Response的header,並叫檔案內容寫入Response的OutputStream中,最後進行Flush重新整理;執行之後可以正常下載檔案,但是發現執行的過程中會報如下的錯誤

System.Web.HttpException (0x80004005): Server cannot set status after HTTP headers have been sent.
at System.Web.HttpResponse.set_StatusCode(Int32 value)
at System.Web.Http.WebHost.HttpControllerHandler.<CopyResponseStatusAndHeadersAsync>d__25.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.WebHost.HttpControllerHandler.<CopyResponseAsync>d__15.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.Http.WebHost.HttpControllerHandler.<ProcessRequestAsyncCore>d__12.MoveNext()
— End of stack trace from previous location where exception was thrown —
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Web.TaskAsyncHelper.EndTask(IAsyncResult ar)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

二、問題原因分析

從錯誤的堆疊可以看到是ASP.NET的Web框架在請求執行的最後要設定StatusCode的時候丟擲了異常;

檢視.NET Framework 4.7.2中Response的StatusCode的程式碼,可以看到首先會檢測_headersWritten欄位,如果欄位值為true,則會丟擲我們看到的異常;

public int StatusCode {
    get {
        return _statusCode;
    }

    set {
        if (_headersWritten)
            throw new HttpException(SR.GetString(SR.Cannot_set_status_after_headers_sent));

        if (_statusCode != value) {
            _statusCode = value;
            _subStatusCode = 0;
            _statusDescription = null;
            _statusSet = true;
        }
    }
}

我們檢視HttpHeaderCollection的Add及SetHeader方法,發現並沒有修改_headersWritten的值,所以直接向Response新增header並不會導致這個異常;

我們檢視Response的Flush方法,發現裡邊會將_headersWritten的值設定為true;但是如果我們註釋掉Flush則就無法下載檔案了;

三、從REST角度理解Web API的返回值

我們使用的是Web API,它是從REST借鑑過來的概念;

REST與技術無關,代表的是一種軟體架構風格。REST是Representational State Transfer的簡稱,中文翻譯為“表徵狀態轉移”;REST從資源的角度來審視整個網路,它將分佈在網路中某個節點中的資源通過URI進行標識,客戶端應用通過URI來獲取資源的表徵,獲得這些表徵致使這些應用程式轉變了狀態。隨著不斷獲取資源的表徵,客戶端應用不斷地在轉變著狀態。

Web API是一種web形式的服務,其需要可以表徵對資源操作的各種情況,也就是需要可以通過返回值來表徵操作的結果;

平時我們都是如下直接返回對應的模型物件

public class ProductsController : ApiController
{
    public IEnumerable<Product> Get()
    {
        return GetAllProductsFromDB();
    }
}

Web API 使用請求中的 Accept 標頭來選擇格式化程式,預設情況下會返回json格式的資料;

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Date: Mon, 27 Jan 2014 08:53:35 GMT
Content-Length: 56

[{"Id":1,"Name":"Yo-yo","Category":"Toys","Price":6.95}]

雖然框架提供了這種簡單的方式,但這種方法的缺點是不能直接返回錯誤程式碼如404;Web API內部還是會將各種形式的返回結果轉化為HttpResponseMessage,最終將HttpResponseMessage轉換為 HTTP 響應訊息;

public class ValuesController : ApiController
{
    public HttpResponseMessage Get()
    {
        HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, "value");
        response.Content = new StringContent("hello", Encoding.Unicode);
        response.Headers.CacheControl = new CacheControlHeaderValue()
        {
            MaxAge = TimeSpan.FromMinutes(20)
        };
        return response;
    } 
}

Web API返回的響應形式如下

HTTP/1.1 200 OK
Cache-Control: max-age=1200
Content-Length: 10
Content-Type: text/plain; charset=utf-16
Server: Microsoft-IIS/8.0
Date: Mon, 27 Jan 2014 08:53:35 GMT

hello

四、使用Web API的方式實現檔案的下載

我們通過HttpResponseMessage來承載檔案內容,並修改對應的header;

[HttpPost]
[Route("Download")]
public HttpResponseMessage Download()
{
    HttpResponse response  = Request.CreateResponse(HttpStatusCode.OK);
    response.Content = new StreamContent(GetFileContent());
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
    response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
    {
        FileName = filename
    };
   
   return response;
}

五、Web API處理管道簡介

整個的Web API處理管道如下圖所示;

首先Web API通過承載URL模式和對應的處理類的HttpWebRoute物件新增到路由集合;當請求到來的時候,會通過路由模組定位到對應的HttpControllerHandler,HttpControllerHandler會將請求轉化成HttpRequestMessage,然後轉發給HttpServer,HttpServer會將請求逐個傳遞個HttpMessageHandler鏈中的物件進行處理,並最終通過HttpControllerDispatcher轉發給實現服務的那個控制器的action;

image

相關文章