一:背景
1. 講故事
前幾天群裡有一位朋友聊到,為什麼我在 Action 中執行一句 Response.Write
之後,後續的 View 就不呈現了,如果腦子中沒有畫面,那就上測試程式碼:
public class HomeController : Controller
{
public IActionResult Index()
{
Response.WriteAsync("hello world!");
return View();
}
}
結果還是挺有意思的,大家都知道,預設情況下會渲染 /Home/Index
對應的 view 頁面,但這裡被 Response.WriteAsync
插了一槓子,氣的 view 都渲染不出來了,那接下來就來找一找 view 為啥這麼生氣?
二:尋找真相
1. 從 Logger 入手
相信很多人都在用 aspnetcore 中的 logger 記錄日誌,為什麼要首選這個 logger 呢?因為它在 web框架 中是一等公民的存在,畢竟底層原始碼各處都嵌入著這玩意哈,隨便找點程式碼:
internal abstract class ActionMethodExecutor
{
private Task ResultNext<TFilter, TFilterAsync>(ref ResourceInvoker.State next, ref ResourceInvoker.Scope scope, [Nullable(2)] ref object state, ref bool isCompleted) where TFilter : class, IResultFilter where TFilterAsync : class, IAsyncResultFilter
{
ResourceInvoker.ResultExecutingContextSealed resultExecutingContext3 = this._resultExecutingContext;
this._diagnosticListener.BeforeOnResultExecuting(resultExecutingContext3, tfilter);
this._logger.BeforeExecutingMethodOnFilter(filterType, "OnResultExecuting", tfilter);
tfilter.OnResultExecuting(resultExecutingContext3);
this._diagnosticListener.AfterOnResultExecuting(resultExecutingContext3, tfilter);
this._logger.AfterExecutingMethodOnFilter(filterType, "OnResultExecuting", tfilter);
if (this._resultExecutingContext.Cancel)
{
this._logger.ResultFilterShortCircuited(tfilter);
this._resultExecutedContext = new ResourceInvoker.ResultExecutedContextSealed(resultExecutingContext3, this._filters, resultExecutingContext3.Result, this._instance)
{
Canceled = true
};
goto IL_39E;
}
}
}
而且大家想想,這種寫法特別奇葩,我想底層框架中的 logger 定會有所反饋,接下來在啟動程式的時候採用 WebApplication1
的模式啟動,如下圖:
啟動後,在控制檯上可以看到一堆報錯資訊:
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: E:\net5\WebApplication1\WebApplication1
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Headers are read-only, response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)
at Microsoft.AspNetCore.Http.DefaultHttpResponse.set_ContentType(String value)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ActionContext actionContext, IView view, ViewDataDictionary viewData, ITempDataDictionary tempData, String contentType, Nullable`1 statusCode)
at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.ExecuteAsync(ActionContext context, ViewResult result)
at Microsoft.AspNetCore.Mvc.ViewResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeResultAsync>g__Logged|21_0(ResourceInvoker invoker, IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
異常資訊非常明顯:Headers are read-only, response has already started
,大概就是說,header是隻讀的,response已是啟動狀態了,從呼叫堆疊的 ViewExecutor.ExecuteAsync
處可看出,程式碼準備渲染 view,在 set_ContentType 處遭遇異常,結束了後續渲染流程。
接下來一起看下,為什麼會觸發這個異常???
三: 除錯原始碼尋找異常的原因
1. dnspy 除錯
除了從異常堆疊中找到最早的異常程式碼處,這裡還說一個小技巧,使用 ndspy 的 異常斷點功能,在異常設定皮膚 定位 InvalidOperationException
異常即可。
接下來就可以讓程式跑起來,當異常丟擲時會自動斷下來。
仔細看一下圖中的文字標註,還是很好理解的,接下來繼續追一下: response.ContentType = contentType2;
內部都做了什麼。
public override string ContentType
{
get
{
return this.Headers[HeaderNames.ContentType];
}
set
{
if (string.IsNullOrEmpty(value))
{
this.HttpResponseFeature.Headers.Remove(HeaderNames.ContentType);
return;
}
this.HttpResponseFeature.Headers[HeaderNames.ContentType] = value;
}
}
可以看到 內部是給 this.HttpResponseFeature.Headers
賦值的,繼續往下追:
從圖中可以看到,最後的 HttpHeader._isReadOnly =true
導致異常的發生,罪魁禍首哈,接下來研究下這句 HttpHeader._isReadOnly=true
是何時被賦值的。
2. _isReadOnly=true 何時發生
這個問題就簡單多了,必定是 Response.WriteAsync("hello world!");
造成了 _isReadOnly=true ,在 HttpHeader 下有一個 SetReadOnly 方法用於對 _isReadOnly 欄位的封裝,程式碼如下:
internal abstract class HttpHeaders
{
public void SetReadOnly()
{
this._isReadOnly = true;
}
}
接下來在該方法處下一個斷點,繼續除錯,如下圖:
從圖中可看到,原來 Response.WriteAsync("hello world!")
是可以封鎖 HttpHeaders的,後續任何再對 HttpHeader 的操作都是無效的。。。
其實大家也可以想一想,不同的response,肯定會有不同的 header,要想疊加的話這輩子都不可能的,只能讓後面的報錯,如下:
1. response:
HTTP/1.1 200 OK
Date: Mon, 19 Oct 2020 14:37:54 GMT
Server: Kestrel
Transfer-Encoding: chunked
c
hello world!
2. view:
HTTP/1.1 200 OK
Date: Mon, 19 Oct 2020 14:39:01 GMT
Content-Type: text/html; charset=utf-8
Server: Kestrel
Content-Length: 2239
四: 總結
這篇就是對群聊天過程中丟擲問題的個人探究,一家之言,不過挺有意思,大家也可以多用用除錯工具尋找問題,證明問題,紙上得來終覺淺,絕知此事要躬行,好了,希望本篇對您有幫助!
更多高質量乾貨:參見我的 GitHub: dotnetfly