1

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 <a href=\"" + url + "\">here</a>.</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?

Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Which browsers don't support 301 redirects? – jrummell Mar 05 '13 at 21:26
  • @jrummell: I know of a lot of plugins (with the evolution of tinyurl, etc.) that halt redirects. though the browser supports it, the user elects to opt-out for security reasons. – Brad Christie Mar 05 '13 at 21:27
  • Is that supposed to protect users from phishing? – jrummell Mar 05 '13 at 21:30
  • @jrummell: Phishing and attempts to forward people off to a site with a 0-day attack. (I personally have mine on to avoid hops of more than 1). – Brad Christie Mar 05 '13 at 21:35
  • @NightOwl: Is the view fixed, based on the redirected route or something else? You should be able to spin up a new ViewContext and dump it out (assuming you know what you want to display). – Brad Christie Mar 05 '13 at 21:37
  • This looks promising: http://approache.com/blog/render-any-aspnet-mvc-actionresult-to/ – Chris Pratt Mar 05 '13 at 21:46
  • @BradChristie: It will just be a normal view with a message like "click here if you aren't redirected automatically", but I would like to retrieve it directly somehow rather than hitting the router again. It sounds like you are on the right track with ViewContext, but I need to see an example. – NightOwl888 Mar 05 '13 at 21:48
  • @NightOwl888: Take a look at [MvcMailer](https://github.com/smsohan/MvcMailer/tree/master/Mvc.Mailer) and how they use the context to generate a view and return it as a string. It's essentialy what you want, but you'll modify the `RouteData` to use the controller/action of your choosing. – Brad Christie Mar 05 '13 at 22:14

2 Answers2

1

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.

Community
  • 1
  • 1
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
0

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.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • This looks promising. As you see by my method signature above, it accepts HttpContextBase as a parameter. However, I am curious if the request context call is going to hit the router again (keep in mind, this redirect code will itself be in the router). – NightOwl888 Mar 05 '13 at 22:31
  • I am not quite sure, trying it will provide you with a definite answer to that. – Darin Dimitrov Mar 05 '13 at 22:31
  • No joy. I am getting a "System.Threading.LockRecursionException: Recursive read lock acquisitions not allowed in this mode." when trying to resolve some of the links on the layout page. It is being thrown from within the MVC framework's RouteCollectionExtensions.FilterRouteCollectionByArea() method, which is called by ActionLink(). – NightOwl888 Mar 06 '13 at 01:16