asp.netasp.net-mvcelmahhttpcontextserver.transfer

HttpContext.Current.Items cleared via responseMode="ExecuteURL"?


I avoid the default ASP.NET approach of redirecting on errors (as many people do). Clean AJAX code and SEO are among the reasons.

However, I'm using the following method to do it, and it seems that I may lose HttpContext.Current.Items in the transfer?

<httpErrors errorMode="Custom" existingResponse="Replace">
    <remove statusCode="401" />
    <remove statusCode="403" />
    <remove statusCode="404" />
    <remove statusCode="500" />
    <error statusCode="401" responseMode="ExecuteURL" path="/Account/SignIn" />
    <error statusCode="403" responseMode="ExecuteURL" path="/Site/Forbidden" />
    <error statusCode="404" responseMode="ExecuteURL" path="/Site/NotFound" />
    <error statusCode="500" responseMode="ExecuteURL" path="/Site/Error" />
</httpErrors>

I assumed it just performed a Server.Transfer() under the covers, which I understand preserves Items. (See: Scope of HttpContext.Current.Items and http://weblog.west-wind.com/posts/2010/Jan/20/HttpContextItems-and-ServerTransferExecute )

But I'm also capturing something in Items before the "ExecuteURL", and retrieving/outputting it after the transfer (or whatever it is), and it seems to disappear. I've watched it go into the Items collection, I see the Count raise to 5, and then when the value is retrieved there are only 2 items in the collection.

What is going on?


If you'd like to understand more about what I'm doing and recommend an alternate implementation, I'm open to it. I'm using this to push the ELMAH Error Id into a ViewModel in a way that is free from race conditions. (i.e. a common workaround for this that I'm replacing is to merely display the most recent error.) Here's my code:

Global.asax

protected void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args) {
    ElmahSupplement.CurrentId = args.Entry.Id;
}

void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs e) {
    if (ElmahSupplement.IsNotFound(e.Exception)) {
        ElmahSupplement.LogNotFound((e.Context as HttpContext).Request);
        e.Dismiss();
    }
}

SiteController.cs

public virtual ActionResult Error() {
    Response.StatusCode = 500;
    return View(MVC.Site.Views.Error, ElmahSupplement.CurrentId);
}

ElmahSupplement.cs

public class ElmahSupplement {
    // TODO: This is a rather fragile way to access this info
    private static readonly Guid contextId = new Guid("A41A67AA-8966-4205-B6C1-14128A653F21");

    public static string CurrentId {
        get { 
            return
                // Elmah 1.2 will fail to log when enumerating form values that raise RequestValidationException (angle brackets)
                // https://code.google.com/p/elmah/issues/detail?id=217
                // So this id could technically be empty here
                (HttpContext.Current.Items[contextId] as string);
        }
        set {
            HttpContext.Current.Items[contextId] = value;
        }
    }

    public static void LogNotFound(HttpRequest request) {
        var context = RepositoryProxy.Context;
        context.NotFoundErrors.Add(new NotFoundError {
            RecordedOn = DateTime.UtcNow,
            Url = request.Url.ToString(),
            ClientAddress = request.UserHostAddress,
            Referrer = request.UrlReferrer == null ? "" : request.UrlReferrer.ToString()
        });
        context.SaveChanges();
    }

    public static bool IsNotFound(Exception e) {
        HttpException he = e as HttpException;
        return he != null && he.GetHttpCode() == 404;
    }
}

Solution

  • I've followed a trace and determined the following. Some is loosely inferred.

    The CustomErrorModule (in the IIS module stack) receives the SEND_RESPONSE notification.

    The HttpStatus is 500, so it clones the context, sets a new URL (according the the matching custom error rule), and executes the request on this context (see ExecuteRequest).

    The purpose of HttpContext.Items per documentation is:

    Gets a key/value collection that can be used to organize and share data between an IHttpModule interface and an IHttpHandler interface during an HTTP request.

    Viewing this function definition critically, of course, there is only "HTTP request". However, it seems likely that the Items dictionary is itself an item in a dictionary keyed on the HttpContext, which is a unique (cloned) reference in this executing child request. The trace shows the full pipeline (all modules, e.g. duplicate authentication) being run for this ExecuteURL, so this isolated context is of course required.

    From unmanaged code, it is trivial to GetParentContext. However, from managed code this hierarchy is not available. So, I'm left without a way to retrieve the original Items.

    As an alternate solution, it might be functional to leverage a Global.asax variable, since my tests showed the child request sharing an ApplicationInstance, but I'm not certain client access to this is necessarily sequential.

    Another, possibly better approach, would be to avoid re-running the entire pipeline; to never exit the MVC handler (e.g. Controller.OnException and TransferToAction). However, this prevents implementing a Single-Point-of-Truth for error page configuration, since errors can also be raised outside of MVC's awareness.