I have an aspx page where I’m allowing a user to upload a file and I want to cap the max file upload size to be 10MB. IIS7, .NET 3.5. I have the following configured in my web.config file:
<location path="foo.aspx">
<system.web>
<!-- maxRequestLength: kbytes, executionTimeout:seconds -->
<httpRuntime maxRequestLength="10240" executionTimeout="120" />
<authorization>
<allow roles="customRole"/>
<!-- Deny everyone else -->
<deny users="*"/>
</authorization>
</system.web>
<system.webServer>
<security>
<requestFiltering>
<!-- maxAllowedContentLength: bytes -->
<requestLimits maxAllowedContentLength="10240000"/>
</requestFiltering>
</security>
<handlers accessPolicy="Read, Script">
<add name="foo" path="foo.aspx" verb="POST"
type="System.Web.UI.PageHandlerFactory"
preCondition="integratedMode" />
</handlers>
</system.webServer>
</location>
I have a custom error handling module that implements IHttpModule. I’ve found that when maxRequestLength is exceeded, HttpApplication.Error does indeed get raised. However when I play with maxAllowedContentLength, the HttpApplication.Error event isn’t being raised and the user gets redirected to a 404.13 page. I've attached with Visual Studio with first chance exceptions turned on nothing is being thrown.
My first thought is to check the header content length in an earlier event – are there recommendations/best practices of where I do this? PostLogRequest? EndRequest?
After looking at the ASP.NET Application Life Cycle Overview for IIS 7.0 and doing my own experimentation, I'm assuming that the request validation is done internally by IIS before any of the events are raised.
It looks like only LogRequest, PostLogRequest, EndRequest, PreSendRequestContent, and PreSendRequestHeaders are raised after the internal validation with this error.
I've decided to attach an event handler to the HttpApplication.EndRequest event in my custom error handler and check for the 404.13 status code on POST and handle as I need it to be handled, which in my case is to redirect to the calling page which will check Server.GetLastError() and display a friendly error to the end user.
private void application_EndRequest(object sender, EventArgs e)
{
HttpRequest request = HttpContext.Current.Request;
HttpResponse response = HttpContext.Current.Response;
if ((request.HttpMethod == "POST") &&
(response.StatusCode == 404 && response.SubStatusCode == 13))
{
// Clear the response header but do not clear errors and
// transfer back to requesting page to handle error
response.ClearHeaders();
HttpContext.Current.Server.Transfer(
request.AppRelativeCurrentExecutionFilePath);
}
}
I'd welcome feedback on this approach and alternatives.
The simplest method is to handle it in the OnError method of page itself.
I think this works only in .NET 4.0, since the WebEventCode property is documented as NEW in .NET 4.0.
protected override void OnError(EventArgs e)
{
Exception err = Server.GetLastError();
if (err is HttpException)
{
if ((err as HttpException).WebEventCode == 3004)
{
Context.Items["error"] = "File exceeded maximum allowed length.";
Server.Transfer( Context.Request.Url.LocalPath );
return;
}
}
base.OnError(e);
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
if (!IsPostBack)
{
string error = Context.Items["error"] as string;
if (!string.IsNullOrEmpty( error ))
showErrorMessage( error );
}
}
What I did was:
get the last error with Server.GetLastError
check that it's a "Maximum request length exceeded." error (WebEventCode == 3004).
added a value to Context.Items collections to flag the request as an error
transfer the request back to the page itself with Server.Transfer(Context.Request.Url.LocalPath)
the page's OnLoad method checks for the error flag and displays a message if present
This ensures the error is handled entirely on the requested page, and the page is capable of reporting errors.
Also note that while the browser will eventually receive a proper response, the browser may take its time uploading the entire request before it handles the server's response and displays it. This behavior is probably defined as part of the server/browser interaction in the HTTP protocol, so probably not much can be done about that.
Related
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;
}
}
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.
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.
I faced weird situation where my session variable set it to null once after coming back from PayPal.
In my scenario before redirect page to PayPal i assign value to session.
public string sessionToken
{
get
{
if (Session["token"] != null)
{
return Session["token"].ToString();
}
else
return string.Empty;
}
set
{
Session["token"] = value;
}
}
calling paypal:
bool ret = payPalCaller.ShortcutExpressCheckout(amt, ref token, ref retMsg, ref status);
if (ret)
{
sessionToken = token;
Response.Redirect(retMsg,false);
}
after user complete paypal (if user takes some time to complete txn) and return back to sucess page and from there i'm trying to access above session variable, then that value is empty. but if i press ctrl+f5 few times then it get value.
what is the problem in here?
in my development pc, this is working fine and problem occurs when i hosted in server. (IIS 6)
My web config configuration as follows:
<configuration>
<location path="RegisterUser.aspx">
<system.web>
<authorization>
<allow users="?" />
</authorization>
<httpRuntime executionTimeout="43200" maxRequestLength="104856" />
<sessionState timeout="3600" mode="InProc" cookieless="false"></sessionState>
<customErrors mode="ON" />
</system.web>
</location>
</configuration>
EDIT:
i have used similar code in Checkout and Payment with PayPal. i found this weird question mentioned in the comment section, but no reply for that question as well.
If pressing ctrl+F5 fixes the issue for you then your success page may just be cached. Try disabling cache on your success page, that might just do the trick. You can refer to this link for reference: Disabling browser caching for all browsers from ASP.NET
How can I replace the standard error page in the case the request length exceeds the one specified in maxQueryStringLength and show the user something more friendly?
Note: Although as an HttpException this falls into generic 400th error, I want to separate the QueryLength condition and show a very specific page for this specific error. So, I cannot use "customErrors" section to indicate a page, but rather need to filter this programmatically. The problem with the below is it doesn't work.
protected virtual void Application_Error(Object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
if (logs != null) logs.ExceptionMsg("Application exception", this, ex);
var httpex = ex as System.Web.HttpException;
if (httpex != null && httpex.ErrorCode == -2147467259)
{
Server.ClearError();
Server.TransferRequest(#"~/main/maxlengthexceeded", false);
}
}
The problem is that Server.TransferRequest does not work. Is there an alternative to how I can tell ASP.NET which page to load?
if you are able to catch the error type/number you are getting, then you can configure a different error/redirect page only for that, here an example of configuration in the web.config:
<configuration>
<system.web>
<customErrors defaultRedirect="GenericError.htm"
mode="RemoteOnly">
<error statusCode="500"
redirect="InternalError.htm"/>
</customErrors>
</system.web>
</configuration>
you can see the full article here: Displaying a Custom Error Page
I have the following code in the web.config file for my ASP.NET C# app that's hosed on Azure:
<!-- Turn on Custom Errors -->
<!-- Switch the mode to RemoteOnly for Retail/Production -->
<!-- Switch the mode to On for to see error pages in the IDE during development -->
<customErrors mode="On" defaultRedirect="ErrorPage.aspx">
<error statusCode="403" redirect="ErrorPage403.aspx"/>
<error statusCode="404" redirect="ErrorPage404.aspx"/>
</customErrors>
This works great for errors when I'm hitting my site site natively (http://ipredikt.com/ErrorPage.aspx), but I also have a Facebook version of the app in which all of the pages use a different MasterPage and hence a different URL (http://ipredikt.com/ErrorPageFB.aspx).
Is it possible to modify the customError redirect values at runtime when I'm running in Facebook app mode, as if I had the following settings in web.config:
<customErrors mode="On" defaultRedirect="ErrorPageFB.aspx">
<error statusCode="403" redirect="ErrorPage403FB.apx"/>
<error statusCode="404" redirect="ErrorPage404FB.apx"/>
</customErrors>
I don't think I can set this at the Application scope since it's individual pages in my app that have knowledge of whether they are running in Facebook mode.
Hi I think what you can do is make another redirect inside your custom error page acording to the referrer - Request.UrlReferrer sometime the referrer is null so make sure you deal with that
So here's a brute force solution. I'm using this on the page for the non-Facebook mode 404 errors:
protected override void OnInit(System.EventArgs e)
{
// If the user tries, for example, to navigate to" /fb/foo/bar
// then the Request.Url.Query will be as follows after the 404 error: ?aspxerrorpath=/fb/foo/bar
string queryString = Request.RequestContext.HttpContext.Request.Url.Query;
string[] str = queryString.Split('=');
if (str.Length > 0)
{
string[] str2 = str[1].Split('/');
if (str2.Length > 1)
{
string test = str2[1].ToLowerInvariant();
if (test == "fb")
{
string pathAndQuery = Request.RequestContext.HttpContext.Request.Url.PathAndQuery;
string absolutePath = Request.RequestContext.HttpContext.Request.Url.AbsolutePath;
string mungedVirtualPath = pathAndQuery.Replace(absolutePath, "/ErrorPage404FB.aspx");
Response.Redirect(mungedVirtualPath);
}
}
}
base.OnInit(e);
}
Hardly ideal, but it works.
"Facebook mode" seems like something you could track in Session, which would be accessible in ErrorPage.aspx to trigger a transfer to ErrorPageFB.aspx.
Update - you can clean up your brute-force solution quite a bit by using Request.QueryString:
protected override void OnInit(System.EventArgs e)
{
// If the user tries, for example, to navigate to" /fb/foo/bar
// then the Request.Url.Query will be as follows after the 404 error: ?aspxerrorpath=/fb/foo/bar
var requestedPath = Request.RequestContext.HttpContext.Request.QueryString["aspxerrorPath"];
if (requestedPath.StartsWith("/fb/", StringComparison.OrdinalIgnoreCase))
{
var requestedUrl = Request.RequestContext.HttpContext.Request.Url;
var pathAndQuery = requestedUrl.PathAndQuery;
var absolutePath = requestedUrl.AbsolutePath;
var mungedVirtualPath = pathAndQuery.Replace(absolutePath, "/ErrorPage404FB.aspx");
Response.Redirect(mungedVirtualPath);
}
base.OnInit(e);
}
Does Request.RequestContext.HttpContext.Request actually return a different instance than simply Request?
The easy way to do it is using session but if you don't use session on you website you can always use cookie and when the user arrive to the error page examine the cookie and decide if you want to redirect him to a new error page
when handling 404 errors in ASP.NET is it ok to set 404 errors to redirect to a page that sends the 404 response code to the browser or should server.transfer be used so the 404 header can be sent to the browser while the url remains the same?
customErrors statusCode="404" results in a 302 temporary redirect then a 404 (if you've set that in your 404 page's code).
Therefore, the following should do it for you in your global.asax or error HttpModule:
protected void Application_Error(Object sender, EventArgs e)
{
Exception exception = Server.GetLastError();
if (exception is HttpUnhandledException)
{
if (exception.InnerException == null)
{
Server.Transfer(ERROR_PAGE_LOCATION, false);
return;
}
exception = exception.InnerException;
}
if (exception is HttpException)
{
if (((HttpException)exception).GetHttpCode() == 404)
{
Server.ClearError();
Server.Transfer(NOT_FOUND_PAGE_LOCATION);
return;
}
}
if (Context != null && Context.IsCustomErrorEnabled)
Server.Transfer(ERROR_PAGE_LOCATION, false);
else
Log.Error("Unhandled exception trapped in Global.asax", exception);
}
Edit: Oh, and Best way to implement a 404 in ASP.NET put me on the road to the imperative Server.ClearError();
See http://www.andornot.com/blog/post/Handling-404-errors-with-ASPNET.aspx for a post I did that covers all of this.
I would use the customerrors section of the web.config, then you can specify the page you want 404 to go to.
<configuration>
<system.web>
<customErrors mode="On" defaultRedirect="Error.aspx">
<error statusCode="404" redirect="404Error.aspx" />
</customErrors>
</system.web>
</configuration>
On the receiving page if you want to still send the 404 you can place this in the page_load event:
Response.Status = "404 Not Found";
Response.Redirect will do 302 first than 404 on redirected page.
Server.Transfer will keep the URL, so it is 404 on requested page.
I think it all comes down SEO. I suggest using Server.Transfer as it is more clear to browser/search engine that the requested URL is not found. If you use Response.Redirect requested page is 'temporarily' redirected to a not found page. That's not good... 302 is not a good idea.
My advice is to let the ASP.NET process do the work for you based on your web.config but if you really want to to this in code you should stick with Server.Transfer cause it will save you a postback.