为啥 Response.Write 后,View就不渲染了?
一:背景
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:?chunkedc 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四:總結
這篇就是對群聊天過程中拋出問題的個人探究,一家之言,不過挺有意思,大家也可以多用用調試工具尋找問題,證明問題,紙上得來終覺淺,絕知此事要躬行,好了,希望本篇對您有幫助!
總結
以上是生活随笔為你收集整理的为啥 Response.Write 后,View就不渲染了?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: eShopOnContainers 知多
- 下一篇: 如何写一段死锁代码