I am attempting to write a 301 redirect scheme using a custom route (class that derives from RouteBase) similar to Handling legacy url's for any level in MVC. I was peeking into the HttpResponse class at the RedirectPermanent() method using reflector and noticed that the code both sets the status and outputs a simple HTML page.
this.StatusCode = permanent ? 0x12d : 0x12e;
this.RedirectLocation = url;
if (UriUtil.IsSafeScheme(url))
{
url = HttpUtility.HtmlAttributeEncode(url);
}
else
{
url = HttpUtility.HtmlAttributeEncode(HttpUtility.UrlEncode(url));
}
this.Write("<html><head><title>Object moved</title></head><body>\r\n");
this.Write("<h2>Object moved to here.</h2>\r\n");
this.Write("</body></html>\r\n");
This is desirable, as in my experience not all browsers are configured to follow 301 redirects (although the search engines do). So it makes sense to also give the user a link to the page in case the browser doesn't go there automatically.
What I would like to do is take this to the next level - I want to output the result of an MVC view (along with its themed layout page) instead of having hard coded ugly generic HTML in the response. Something like:
private void RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.Clear();
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
// Output a Custom View Here
response.Write(The View)
response.End();
}
How can I write the output of a view to the response stream?
Additional Information
In the past, we have had problems with 301 redirects from mydomain.com to www.mydomain.com, and subsequently got lots of reports from users that the SSL certificate was invalid. The search engines did their job, but the users had problems until I switched to a 302 redirect. I was actually unable to reproduce it, but we got a significant number of reports so something had to be done.
I plan to make the view do a meta redirect as well as a javascript redirect to help improve reliability, but for those users who still end up at the 301 page I want them to feel at home. We already have custom 404 and 500 pages, why not a custom themed 301 page as well?
It turns out I was just over-thinking the problem and the solution was much simpler than I had envisioned. All I really needed to do was use the routeData to push the request to another controller action. What threw me off was the extra 301 status information that needed to be attached to the request.
private RouteData RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.CacheControl = "no-cache";
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "Redirect301";
routeData.Values["url"] = destinationUrl;
return routeData;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
// Do stuff here...
if (this.IsDefaultUICulture(cultureName))
{
var urlWithoutCulture = url.ToString().ToLowerInvariant().Replace("/" + cultureName.ToLowerInvariant(), "");
return this.RedirectPermanent(urlWithoutCulture, httpContext);
}
// Get the page info here...
if (page != null)
{
result = new RouteData(this, new MvcRouteHandler());
result.Values["controller"] = page.ContentType.ToString();
result.Values["action"] = "Index";
result.Values["id"] = page.ContentId;
}
return result;
}
I simply needed to return the RouteData so it could be processed by the router!
Note also I added a CacheControl header to prevent IE9 and Firefox from caching the redirect in a way that cannot be cleared.
Now I have a nice page that displays a link and message to the user when the browser is unable to follow the 301, and I will add a meta redirect and javascript redirect to the view to boot - odds are the browser will follow one of them.
Also see this answer for a more comprehensive solution.
Assuming you have access to the HttpContextBase here's how you could render the contents of a controller action to the response:
private void RedirectPermanent(string destinationUrl, HttpContextBase httpContext)
{
var response = httpContext.Response;
response.Clear();
response.StatusCode = 301;
response.RedirectLocation = destinationUrl;
// Output a Custom View Here (HomeController/SomeAction in this example)
var routeData = new RouteData();
routeData.Values["controller"] = "Home";
routeData.Values["action"] = "SomeAction";
IController homeController = new HomeController();
var rc = new RequestContext(new HttpContextWrapper(context), routeData);
homeController.Execute(rc);
response.End();
}
This will render SomeAction on HomeController to the response.
Related
I have an ActionFilter that is successfully capturing page views along with important information from the request. The primary key of this captured entry is then associated with an activity (i.e: Successful Login).
I am trying to now automatically capture Redirects (i.e: RedirectToAction) and associate this with the page view as well. I believe this can be done in my ActionFilter that is capturing page views, but I am unsure if there is a way to tell from the OnActionExecuting context whether or not the GET request is coming from a redirect.
Is there a way to tell from an HttpRequest / ActionExecutingContext(or ActionExecutedContext) whether or not the page is coming from a redirect?
Thanks!
RedirectToAction returns a RedirectToRouteResult
In the OnActionExecuted method of your filter -
if (filterContext.Result is RedirectResult)
{
// It was a RedirectResult
var result = filterContext.Result as RedirectResult;
var url = UrlHelper.GenerateContentUrl(result.Url, filterContext.HttpContext);
}
else if (filterContext.Result is RedirectToRouteResult)
{
// It was a RedirectToRouteResult
var result = filterContext.Result as RedirectToRouteResult;
var url = UrlHelper.GenerateUrl(result.RouteName, null, null, result.RouteValues, RouteTable.Routes, filterContext.RequestContext, false);
}
So I have below mechanism to handle exception in my mvc application. Basically, I am trying to handle it on Application_EndRequest() in Global.asax.cs file.
protected void Application_EndRequest()
{
int status = Convert.ToInt32(Context.Response.StatusCode);
if (status != 200)
{
var controller = "Error";
var action = "Index";
Response.Clear();
var rd = new RouteData();
//rd.DataTokens["area"] = "AreaName"; // In case controller is in another area
switch (status)
{
case 404:
action = "NotFound";
break;
default:
action = "UnknownError";
break;
}
rd.Values["controller"] = controller;
rd.Values["action"] = action;
IController c = new ErrorController();
c.Execute(new RequestContext(new HttpContextWrapper(Context), rd));
}
}
Everything is working fine and it displays NotFound view when status is 404 and UnknownView for all the other default 5xx errors. Now when I see the url it does not change:
Ex: I try to visit a view say domainname.com/Gallery/Img which does not exist and I will be redirected to NotFound View but the url remains as it is i.e.domainname.com/Gallery/Img. I want this to be changed to like domainname.com/Error/NotFound
A image below to show the problem:
Now few doubts too on this:
If I change the url on redirection to errorview will it appear in google search when searched for my website.
Which way is better? changing the url? or keeping it as it is? the reason being is I do not want neither my error urls nor the non-existing urls which the user searched for, to appear in the google search while users search for my site!
Any help with little bit of explanations to understand the concept properly on this is highly appreciated!
I have been searching for a solution to this issue for over 10 hours with no answer. In my application I am using the [requirehttps] attribute. When clicking an action method decorated with this attribute I get “cannot display the webpage" in IE. After digging into that issue I saw that I was receiving infinite 302 calls in Fiddler, which would eventually timeout and cause that error. So I decided to create a custom attribute and physically create the https call. I am using IIS Express and successfully created a certificate and binded the port. If I call this URL directly through the browser everything works fine. This is the code I have been using to redirect the request.
public class HttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
public bool RequireSecure = false;
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext){
var builder = new UriBuilder(HttpContext.Current.Request.Url);
if (RequireSecure){
// redirect to HTTP version of page
builder.Scheme = Uri.UriSchemeHttps;
builder.Port = 44300;
filterContext.Result = new RedirectResult(builder.Uri.ToString());
}
else{
// non secure requested
if (filterContext.HttpContext.Request.IsSecureConnection){
HandleNonHttpRequest(filterContext);
}
}
}
protected virtual void HandleNonHttpRequest(AuthorizationContext filterContext){
if (String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)){
// redirect to HTTP version of page
string url = "http://" + filterContext.HttpContext.Request.Url.Host + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
}
}
When I set a breakpoint to find out what the builder value is, it is correct. And it is from here I receive an infinite redirect loop. The strange thing is that the first request is never correct when I look at the URL within the browser. It is as if UriBuilder is not sending the correct URL. Any ideas? This is driving me nuts.
The redirect loop is occurring because the code is only checking if the HTTPS redirect is required, not if the current request is already HTTPS (i.e. the redirect has already happened).
if (RequireSecure && !filterContext.HttpContext.Request.IsSecureConnection){
// redirect to HTTP version of page
builder.Scheme = Uri.UriSchemeHttps;
builder.Port = 44300;
filterContext.Result = new RedirectResult(builder.Uri.ToString());
}
else{
// non secure requested
if (filterContext.HttpContext.Request.IsSecureConnection){
HandleNonHttpRequest(filterContext);
}
}
Although the RequireHttps should work correctly for this, unless you need to redirect to the port specified.
EDIT:
Refactored attribute
public class HttpsAttribute : System.Web.Mvc.RequireHttpsAttribute
{
public bool RequireSecure = false;
public override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
{
var requestUri = HttpContext.Current.Request.Url;
var requestIsSecure = HttpContext.Current.Request.IsSecureConnection;
if (RequireSecure && !requestIsSecure)
filterContext.Result = Redirect(requestUri, Uri.UriSchemeHttps, 44300);
else if (!RequireSecure && requestIsSecure)
filterContext.Result = Redirect(requestUri, Uri.UriSchemeHttp, 80);
}
private RedirectResult Redirect(Uri uri, string scheme, int port)
{
return new RedirectResult(new UriBuilder(uri) { Scheme = scheme, Port = port }.Uri.ToString());
}
}
I have a method decorated with two custom ActionFilterAttribute.
[RequiresAuthentication(Order = 1)]
[ToonAction(Order = 2)]
public ActionResult Browse(...
RequiresAuthentication attribute is coming from this article
Inside RequiresAuthentication, on it's OnActionExecuting I do:
filterContext.HttpContext.Response.Redirect(loginUrl, true);
The line is get executed, and the arguments are all as expected. The problem is that after executing the line above, I get next attribute (ActionFilterAttribute) executed, as if redirect didn't work, it just continues executing the request, instead of simply redirecting browser.
Question: what else do I need to do to make the request handler
This is a complete method:
public override void OnActionExecuting(ActionExecutingContext filterContext) {
//redirect if not authenticated
var identity = filterContext.HttpContext.User.Identity;
if (!identity.IsAuthenticated) {
//use the current url for the redirect
string redirectOnSuccess = filterContext.HttpContext.Request.Url.PathAndQuery;
//send them off to the login page
string redirectUrl = string.Format("?ReturnUrl={0}", redirectOnSuccess);
string loginUrl = FormsAuthentication.LoginUrl + redirectUrl;
filterContext.HttpContext.Response.Redirect(loginUrl, true);
// filterContext.Result = new HttpUnauthorizedResult();
// filterContext.HttpContext.Response.StatusCode = 0x191;
}
}
You want to set the Result on the filterContext to a RedirectResult, not do a redirect on the response.
filterContext.Result = new RedirectResult { Url = loginUrl };
EDIT: As #Hunter Daley suggests a better mechanism would be to use the AuthorizeAttribute instead if it works for you. If you do have authentication/authorization scenarios that the AuthorizeAttribute doesn't work for, it would probably be better to derive your custom attribute from it instead of the more generic ActionFilterAttribute. In any event, the correct technique is to set the Result rather than interact with the Response directly. You might want to look at the actual AuthorizeAttribute source at http://www.codeplex.com/aspnet for ideas.
I've got a sample of custom authorization code on my blog, http://farm-fresh-code.blogspot.com, too.
try adding the [Authorize] attribute to your Action methods instead
Add
filterContext.HttpContext.Response.Clear();
at first
and this at End :
filterContext.HttpContext.Response.End();
Hope this helps.
you can use
return RedirectToAction("Index", "Home/Login", new {area = "", returnURL = Request.Url.AbsolutePath});
to stop the current processing, redirect to the desired (login) page and quit the action.
the area route property is needed to get out of the current area if you are in any.
Add this code before you redirect the page.
filterContext.ExceptionHandled = true;
I have some action methods behind an Authorize like:
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(int siteId, Comment comment) {
The problem I have is that I'm sending a request through AJAX to Comment/Create with
X-Requested-With=XMLHttpRequest
which helps identify the request as AJAX. When the user is not logged in and hits the Authorize wall it gets redirected to
/Account/LogOn?ReturnUrl=Comment%2fCreate
which breaks the AJAX workflow. I need to be redirected to
/Account/LogOn?X-Requested-With=XMLHttpRequest
Any ideas how that can be achieved? Any ways to gain more control over what happens when Authorization is requested?
Thanks to Lewis comments I was able to reach this solution (which is far from perfect, posted with my own comments, if you have the fixes feel free to edit and remove this phrase), but it works:
public class AjaxAuthorizeAttribute : AuthorizeAttribute {
override public void OnAuthorization(AuthorizationContext filterContext) {
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest()) {
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
// 2- When leaving Result as a HttpUnauthorizedResult, ASP.Net actually does some nice automatic stuff, like adding a ReturnURL, when hardcodding the URL here, that is lost.
String url = System.Web.Security.FormsAuthentication.LoginUrl + "?X-Requested-With=XMLHttpRequest";
filterContext.Result = new RedirectResult(url);
}
}
}
Recently I ran into exactly the same problem and used the code posted by J. Pablo Fernández
with a modification to account for return URLs. Here it is:
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
override public void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
{
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
HttpRequestBase request = filterContext.HttpContext.Request;
string returnUrl = request.Path;
bool queryStringPresent = request.QueryString.Count > 0;
if (queryStringPresent || request.Form.Count > 0)
returnUrl += '?' + request.QueryString.ToString();
if (queryStringPresent)
returnUrl += '&';
returnUrl += request.Form;
String url = System.Web.Security.FormsAuthentication.LoginUrl +
"?X-Requested-With=XMLHttpRequest&ReturnUrl=" +
HttpUtility.UrlEncode(returnUrl);
filterContext.Result = new RedirectResult(url);
}
}
}
Instead of using the authorize attribute, I've been doing something like the following.
public ActionResult SomeCall(string someData)
{
if (Request.IsAjaxRequest() == false)
{
// TODO: do the intended thing.
}
else
{
// This should only work with AJAX requests, so redirect
// the user to an appropriate location.
return RedirectToAction("Action", "Controller", new { id = ?? });
}
}
I think the right way to handle this would be in your Javascript making the AJAX call.
If the user needs to be authorized (or authenticated as your code implies) and isn't, you should inform them and maybe not allow them to try and comment in the first place.
However, if that doesn't suit your needs.
You could try and write your own authorize action filter, maybe inheriting from the one that comes with the MVC framework but redirects how you want it to. It's fairly straightforward.