HttpContext.Current.Items cleared via responseMode="ExecuteURL"?
Asked Answered
W

2

5

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;
    }
}
Washcloth answered 26/3, 2014 at 13:59 Comment(2)
Your assumption is wrong, ASP.NET MVC no longer uses Server.Transfer and thus it does not behave as you expected. And Server.Transfer does not work with ASP.NET MVC as well, because MVC is rebuilt on async pipeline.Precocious
Akash, you seem to be making two comments, but I understand them to say the same thing, so maybe I misunderstood? Also, although I certainly believe you, I don't really understand the issue with Server.Transfer. Isn't that performed outside the scope of the MVC handler? I guess most importantly, if you have a suggestion on how to address, please make it. 18 hours until the +50 is lost. :(Washcloth
W
0

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.

Washcloth answered 8/4, 2014 at 20:6 Comment(0)
F
1

As explained here, the ExecuteURL generates two requests: the first one throws the exception and the second one generates the error response.

Since Context.Items is cleared between requests, your code always see the 2nd request generated hence the diff between the Items.

Try the sugestion in the post: use system.web > customErrors with redirectMode="ResponseRewrite" instead.

Fipple answered 4/4, 2014 at 17:30 Comment(3)
I'm afraid that doesn't answer why context is clearing. If there is actually a second 'virtual' client request materialized, what is the mechanism? So I still don't have enough information to resolve it. I'd still accept the answer if the suggested workaround was applicable, but it does not work with MVC. Thank you for the link though (+1). I'll take a look at the trace.Washcloth
I should clarify, that since a major function of context Items is to share information between handlers and modules, a second request would have to start back at the very beginning of the process to lose this information (HttpContext) naturally. At the same time, the IP to respond to would be lost.Washcloth
From my tests, it seems that CustomErrorModule's ExecuteURL generates one child request, running on a cloned HttpContext. It runs the entire pipeline, wrapped by the prior context. However, because this child context is cloned in unmanaged code it does not receive a duplicate of Items.Washcloth
W
0

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.

Washcloth answered 8/4, 2014 at 20:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.