文章出處

博客后臺切換至i.cnblogs.com之后,在日志中發現大量的“無法在發送HTTP標頭之后進行重定向”(Cannot redirect after HTTP headers have been sent)的錯誤信息。

檢查代碼發現問題是由下面的代碼觸發的:

IHttpHandler IHttpHandlerFactory.GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
    context.Response.Redirect("http://i.cnblogs.com/" + 
        context.Request.RawUrl.Substring(context.Request.RawUrl.LastIndexOf("/") + 1));

    //后續也有context.Response.Redirect代碼
    //...
    return PageParser.GetCompiledPageInstance(newurl, path, context);
}

“無法在發送HTTP標頭之后進行重定向”問題來源于Response.Redirect之后,又進行了Response.Redirect。

解決方法很簡單:在Response.Redirect之后立即返回。

IHttpHandler IHttpHandlerFactory.GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
    context.Response.Redirect("http://i.cnblogs.com/" + 
        context.Request.RawUrl.Substring(context.Request.RawUrl.LastIndexOf("/") + 1));
    return null;
    //...    
}

為什么之前沒有加return null呢?因為以前一直以為Response.Redirect會結束當前請求,不會執行Response.Redirect之后的代碼。

現在殘酷的現實說明了不完全是這樣的,那問題背后的真相是什么?讓我們來一探究竟。

由于微軟公開了.NET Framework的源代碼,現在無需再看Reflactor出來的代碼,可以直接下載源代碼用Visual Studio進行查看。

.NET Framework源代碼下載鏈接:http://referencesource.microsoft.com/download.html (相關新聞:微軟開放了.NET 4.5.1的源代碼

用Visual Studio打開DotNetReferenceSource\Source\ndp.sln,搜索HttpResponse.cs,找到Response.Redirect的實現代碼:

public void Redirect(String url)
{
    Redirect(url, true, false);
}

實際調用的是internal void Redirect(String url, bool endResponse, bool permanent) ,傳給endResponse的值的確是true啊,為什么后面的代碼還會執行?

進一步查看internal void Redirect()的實現代碼(省略了無關代碼):

internal void Redirect(String url, bool endResponse, bool permanent) 
{
    //...

    Page page = _context.Handler as Page;
    if ((page != null) && page.IsCallback) {
        //拋異常
    }

    // ... url處理

    Clear(); //Clears all headers and content output from the buffer stream.

    //...
    this.StatusCode = permanent ? 301 : 302; //進行重定向操作
    //...
    _isRequestBeingRedirected = true; 

    var redirectingHandler = Redirecting;
    if (redirectingHandler != null) {
        redirectingHandler(this, EventArgs.Empty);
    }

    if (endResponse)
        End(); //結束當前請求
}

從上面的代碼可以看出,我們要找的真相在End()方法中,繼續看HttpResponse.End()的實現代碼:

public void End() {
    if (_context.IsInCancellablePeriod) {
        AbortCurrentThread();
    }
    else {
        // when cannot abort execution, flush and supress further output
        _endRequiresObservation = true;

        if (!_flushing) { // ignore Reponse.End while flushing (in OnPreSendHeaders)
            Flush();
            _ended = true;

            if (_context.ApplicationInstance != null) {
                _context.ApplicationInstance.CompleteRequest();
            }
        }
    }
}

注意啦!真相浮現了!

以前一直以為的Response.Redirect會結束當前請求,就是上面的AbortCurrentThread()情況,如果將Response.Redirect放在try...catch中就會捕捉到ThreadAbortException異常。

通常情況下,我們在WebForms的Page或MVC的Controller中進行Redirect,_context.IsInCancellablePeriod的值為true,執行的是AbortCurrentThread(),所以不會遇到這個問題。

而我們現在的場景恰恰是因為_context.IsInCancellablePeriod的值為false,為什么會是false呢?

進一步看一下_context.IsInCancellablePeriod的實現:

private int _timeoutState; // 0=non-cancelable, 1=cancelable, -1=canceled

internal bool IsInCancellablePeriod {
    get { return (Volatile.Read(ref _timeoutState) == 1); }
}

根據上面的代碼,觸發這個問題的條件是_timeoutState的值要么是0,要么是-1,根據我們的實際情況,應該是0=non-cancelable。

再來看看我們的實際應用場景,我們是在實現IHttpHandlerFactory接口的GetHandler方法中進行Response.Redirect操作的,也就是說在這個階段_timeoutState的值還沒被設置(默認值就是0)。為了驗證這個想法,繼續看一下_timeoutState在哪個階段設值的。

Shift+F12找到所有引用_timeoutState的地方,在HttpConext中發現了設置_timeoutState的方法BeginCancellablePeriod,實現代碼如下:

internal void BeginCancellablePeriod() {
    // It could be caused by an exception in OnThreadStart
    if (Volatile.Read(ref _timeoutStartTimeUtcTicks) == -1) {
        SetStartTime();
    }

    Volatile.Write(ref _timeoutState, 1);
}

然后再Shift+F12找到了在HttpApplication.ExecuteStep()中調用了BeginCancellablePeriod():

internal Exception ExecuteStep(IExecutionStep step, ref bool completedSynchronously) 
{
    //..
    if (step.IsCancellable) {
        _context.BeginCancellablePeriod(); // request can be cancelled from this point
    }
    //..
}

從上面的代碼可以看出,當step.IsCancellable為true時,會調用BeginCancellablePeriod(),就不會出現這個問題。

而我們用到的IHttpHandlerFactory.GetHandler()所在的IExecutionStep的實現可能將IsCancellable設置為了false。

那IHttpHandlerFactory.GetHandler()是在哪個IExecutionStep的實現中調用的呢?

在園子里木宛城主的一篇寫得非常棒的博文(ASP.NET那點不為人知的事)中找到了答案——MapHandlerExecutionStep:

當執行到MapHandlerExecutionStep時會執行如下代碼獲取最終執行請求:context.Handler = this._application.MapHttpHandler()。HttpApplication對象的MapHttpHandler方法將根據配置文件結合請求類型和URL以調用相應的IHttpHandlerFactory來獲取HttpHandler對象。

我們再回到.NET Framework的源代碼中看一看MapHandlerExecutionStep的實現:

// execution step -- map HTTP handler (used to be a separate module)
internal class MapHandlerExecutionStep : IExecutionStep {
    private HttpApplication _application;

    internal MapHandlerExecutionStep(HttpApplication app) {
        _application = app;
    }

    void IExecutionStep.Execute() {
        //...
    }

    bool IExecutionStep.CompletedSynchronously {
        get { return true;}
    }

    bool IExecutionStep.IsCancellable {
        get { return false; }
    }
}

看到有沒有?IExecutionStep.IsCancellable返回的值是false。

到此,水落石出,真相大白!

請看大屏幕——

由于MapHandlerExecutionStep(調用IHttpHandlerFactory.GetHandler()的地方)返回的IsCancellable的值是false,于是在HttpApplication.ExecuteStep()執行時沒有調用_context.BeginCancellablePeriod()——也就是沒有把_timeoutState設置為1,_context.IsInCancellablePeriod的值就是false。從而造成在Response.Redirect中進行Response.End()時沒有執行AbortCurrentThread()(通常情況下都會執行這個)。于是代碼繼續執行,后面又來一次Response.Redirect,最終引發了——“無法在發送HTTP標頭之后進行重定向”(Cannot redirect after HTTP headers have been sent)。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()