15

为啥 Response.Write 后,View就不渲染了?

 3 years ago
source link: https://segmentfault.com/a/1190000037533647
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一:背景

1. 讲故事

前几天群里有一位朋友聊到,为什么我在 Action 中执行一句 Response.Write 之后,后续的 View 就不呈现了,如果脑子中没有画面,那就上测试代码:

public class HomeController : Controller
    {
        public IActionResult Index()
        {
            Response.WriteAsync("hello world!");
            return View();
        }
    }

BRFBnqm.png!mobile

结果还是挺有意思的,大家都知道,默认情况下会渲染 /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 的模式启动,如下图:

YRrA3eY.png!mobile

启动后,在控制台上可以看到一堆报错信息:

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 异常即可。

yieaMv.png!mobile

接下来就可以让程序跑起来,当异常抛出时会自动断下来。

UJNBFvB.png!mobile

仔细看一下图中的文字标注,还是很好理解的,接下来继续追一下: 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 赋值的,继续往下追:

rAvIza7.png!mobile

从图中可以看到,最后的 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;
    }
}

接下来在该方法处下一个断点,继续调试,如下图:

MZneymi.png!mobile

从图中可看到,原来 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK