in

SharePoint Blogs

The Best Place for SharePoint-related Blogs

Tech MOSS Team

SharePoint 2007 redirect solved: using 301 instead of 302 redirects

Each time you request a Site Collection (http://domain/) or a Site (http://domain/foo/) of your Publishing Site you get redirected to the .aspx">http://domain/Pages/<WelcomePage>.aspx. SharePoint 2007 uses the 302 header (location temporarily moved) for this purpose. Surprisingly even WSS uses the 302 header to redirect a root url to the default.aspx. In comparison ASP.NET uses an internal redirect to render the default page when the root url requested: there is no redirect in this situation.

The whole issue about the 302 headers is that the redirected locations don't get crawled by search spiders which don't follow temporarily moved pages. While it's not really an issue for intranet environments it has major impact on indexing the content of Internet-facing web sites and making them searchable using a search engine.

Looking for an answer I have researched the SharePoint runtime: SPHttpHandler, SPRequestModule and PublishingHttpModule classes. As none of these has given me a clear answer I have noticed that there are multiple references to the Redirect method present in the code which uses the 302 header as well.

To solve the issue I have designed a custom redirect HttpModule which uses 301 headers instead.

The requirements

The module must rewrite all request for a Site Collection or Site. Url's of these request might but don't have to contain trailing slash (/). Furthermore the module must distinct a WSS request from a Publishing Site / Publishing Web request. Also the module has to be aware of Variations if used by the Site Collection.

The work

Firs of all we create a new HttpModule. As we want the redirect to find place as soon as possible we will hook it up in the BeginRequest event. Furthermore we want the module to be the first one to interact with the request. As we use an external assembly we need to define it as the first element in the httpModules section of web.config.

namespace Imtech.SharePoint.Enhancement.HttpModules
{
public class RedirectModule : IHttpModule
{
#region IHttpModule Members

public void Dispose()
{ }

public void Init(HttpApplication context)
{
context.BeginRequest +=
new EventHandler(context_BeginRequest);
}

void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
string requestUrl = app.Request.Url.ToString();
}

#endregion
}
}

Because we will need the Request url later on in quite a few places I have decided to store it in a separate variable.

The first requirement states that the module should redirect only requests for Site Collections and Sites. If the requirement wouldn't have say that the trailing slash is optional you could solve it using a simple if (requestUrl.EndsWith("/")). In our situation we will have to use a Regular Expression in order to figure out whether we need to rewrite the url or not.

Regex regEx =
new Regex(@"^https?://.*(?<itemUrl>/[^/]+\.[^/\.]+)$");
if (regEx.IsMatch(requestUrl))
return;

if (!requestUrl.EndsWith("/",
StringComparison.CurrentCulture))
requestUrl += "/";

If the url matches the regular expression it means it's a page request and should be passed on along the request pipeline unaltered. Later in the module we will combine the request url with the page url. As the trailing slash is optional I have decided to add it at the end if not present - just to be sure that combining the destination url of different parts will produce correct result.

The next requirement is distinction between WSS and Publishing Site requests.

string destinationUrl = String.Empty;

SPSecurity.RunWithElevatedPrivileges(delegate()
{
try
{
using (SPSite site = new SPSite(requestUrl))
{
using (SPWeb web = site.OpenWeb())
{
if (PublishingWeb.IsPublishingWeb(web))
destinationUrl = String.Concat(requestUrl,
publishingWeb.DefaultPage.Url);
else
destinationUrl = String.Concat(requestUrl,
"default.aspx");
}
}
}
catch { }
});

Based on the request url we create a new instance of SPSite and then open the requested web. As we can fail at this point already (for example when passing a list url) I have decided to catch the thrown exception to avoid turning the request into an error message. The distinction itself is quite straight forward and makes use of the IsPublishingWeb method. One important thing: because we are very likely to use the module for anonymous users we need to run the code with elevated privileges: the IsPublishingWeb method requires some extra permission in order to run.

Our last requirement was making the redirect module aware of Variations if used by the Site Collection. Depending on the requirements defined by your customer you might need to implement the standard SharePoint Variation logic which chooses the variation basin on the User Agent language settings. Unfortunately most users are not aware of the existence and usage possibilities of the language settings most of our customers choose to load the Dutch variation by default. If your customer requires the standard SharePoint approach you would need to implement the logic from the VariationRootLanding User Control in the ControlTemplates directory. I will focus on the scenario we're using.

PublishingWeb publishingWeb = PublishingWeb.GetPublishingWeb(web);
if (publishingWeb.DefaultPage.Url.EndsWith("/VariationRoot.aspx",
StringComparison.CurrentCultureIgnoreCase))
{
string defaultPage = String.Empty;
using (SPWeb nlWeb = site.OpenWeb("nl"))
{
defaultPage =
PublishingWeb.GetPublishingWeb(nlWeb).DefaultPage.Url;
}

destinationUrl = String.Concat(requestUrl, "nl/", defaultPage);
}
else
destinationUrl =
String.Concat(requestUrl, publishingWeb.DefaultPage.Url);

In most scenarios the variation redirect finds place at the Site Collection level. The default page of the root web is then set to Pages/VariationRoot.aspx. Knowing this we can check whether we need to use the variation redirect or not. The rest is quite straight-forward: we obtain the Dutch site and its Welcome Page.

The last part is the redirect itself using the 301 header:

if (!String.IsNullOrEmpty(destinationUrl))
{
app.Response.AddHeader("Location", destinationUrl);
app.Response.StatusCode = 301;
}

The destination url might be empty if an exception has occurred during the request processing. We will therefore redirect only if a destination url has been set by our module.

Putting it all together:

namespace Imtech.SharePoint.Enhancement.HttpModules
{
public class RedirectModule : IHttpModule
{
#region IHttpModule Members

public void Dispose()
{ }

public void Init(HttpApplication context)
{
context.BeginRequest +=
new EventHandler(context_BeginRequest);
}

void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
string requestUrl = app.Request.Url.ToString();
Regex regEx =
new Regex(@"^https?://.*(?<itemUrl>/[^/]+\.[^/\.]+)$");
if (regEx.IsMatch(requestUrl))
return;

if (!requestUrl.EndsWith("/",
StringComparison.CurrentCulture))
requestUrl += "/";

string destinationUrl = String.Empty;

SPSecurity.RunWithElevatedPrivileges(delegate()
{
try
{
using (SPSite site = new SPSite(requestUrl))
{
using (SPWeb web = site.OpenWeb())
{
if (PublishingWeb.IsPublishingWeb(web))
{
PublishingWeb publishingWeb =
PublishingWeb.GetPublishingWeb(web);
if (publishingWeb.DefaultPage.Url.
EndsWith("/VariationRoot.aspx",
StringComparison.CurrentCultureIgnoreCase))
{
string defaultPage = String.Empty;
using (SPWeb nlWeb = site.OpenWeb("nl"))
{
defaultPage =
PublishingWeb.GetPublishingWeb(nlWeb)
.DefaultPage.Url;
}

destinationUrl = String.Concat(requestUrl,
"nl/", defaultPage);
}
else
destinationUrl = String.Concat(requestUrl,
publishingWeb.DefaultPage.Url);
}
else
destinationUrl = String.Concat(requestUrl, "default.aspx");
}
}
}
catch { }
});

if (!String.IsNullOrEmpty(destinationUrl))
{
app.Response.AddHeader("Location", destinationUrl);
app.Response.StatusCode = 301;
}
}

#endregion
}
}

To see it working build the project, copy the assembly to the bin directory of your web application and add the following element to the httpModules section of the web.config:

<add name="ImtechRedirectModule"
type="Imtech.SharePoint.Enhancement.HttpModules.RedirectModule" />

Summary

Redirects using the 302 header can form a serious issue on Internet-facing web sites as it comes to indexing the content of a web site. Using custom HttpModules to overrule the standard behavior of SharePoint is a flexible solution for this challenge.
The example above should work good enough in most scenarios. Depending on the requirements of your customers you might need to extend it with some extra functionality like for example standard Variations logic support. Custom HttpModules prove the extensibility and flexibility of SharePoint 2007 and the way it can be made to fit various requirements and scenarios.

Comments

 

Susan Swanger said:

THANK YOU!!!! We have been struggling with this issue on our external site since we implemented it in August 2007. Finally someone has figured out how to fix this. We had incident with Microsoft about this issue but no one ever resolved it or informed us of this solution.

March 20, 2008 8:28 AM
 

Waldek Mastykarz said:

You're welcome. Glad I could help :)

March 21, 2008 12:51 AM
 

Joe Vanco said:

Waldek,

Great article. I am curious, would a variation of this code be used to remove '/pages/' from the address?

I have been directed to try and clean that from the url for SEO.

April 21, 2008 8:42 AM
 

Waldek Mastykarz said:

Unfortunately not. But if you want to have semantic urls in SharePoint I would recommend you to have a look at the Rapid for SharePoint. It's basically a taxonomy framework but it has a url rewriting module which strips the Pages part of the URL.

April 22, 2008 3:46 AM
 

Joe Vanco said:

Thanx..

i just downloaded it and giving it a try

May 23, 2008 8:33 AM
 

Waldek Mastykarz said:

Thank you for your feedback

May 23, 2008 5:08 PM
 

Matt said:

This worked great, but unfortunately had a side effect. SharePoint Designer and InfoPath are both picking up this HttpModule when trying to connect to the site, and subsequently are getting the 301 statuscode which cause both to be unable to connect.

June 5, 2008 9:26 AM
 

Matt said:

I was able to update this HttpModule to ignore InfoPath and SP Designer by placing the following code immediately after the app variable was declared inside BeginRequest. I noticed after looking at the iis logs that SP Designer's user agent was coming through as frontpage.

If anyone has any thoughts, feel free to share:

     string userAgent = app.Request.UserAgent.ToLowerInvariant();

     if (userAgent.Contains("infopath") || userAgent.Contains("frontpage"))

       return;

June 5, 2008 12:33 PM
 

Waldek Mastykarz said:

Thank you for your feedback and sharing the solution :)

June 5, 2008 11:29 PM
 

Phil Wicklund said:

I have a blog that is using the enhanced blog edition (EBE) for the community kit for sharepoint. That system uses xsl to render the content of the posts. Any idea if the same indexing problem exists with EBE? Sure would stink to not have any of my posts being crawled...

Phil

June 18, 2008 8:58 AM
 

Waldek Mastykarz said:

Phil, I don't really see a link between using XSL for generating the presentation layer and having the content crawled? Could you explain it further?

As this blog has been moved to http://blog.mastykarz.nl (http://blog.mastykarz.nl/2008/01/21/sharepoint-2007-redirect-solved-using-301-instead-of-302-redirects/) I would really appreciate it if you reposted you question there including some more explanation. I'll try to answer your question as soon as you do.

June 18, 2008 9:53 AM
 

Tim Dobrinski said:

I have been trying to get this installed in my sandbox all day. I have removed your Variation logic as it is flawed (hardcoded variation label path? That removed the point of variations, no?). Also, you can change the VariationRootLanding logic in the ascx file much easier.

But I have, for testing purposes, hardcoded the destinationUrl just before the re-direct happens to force it to a page that isn’t the default to make sure it's intercepting the redirect. Doesn’t work. Also, even without my forcing the page to a new location, when I ran a desktop application to trace the redirect paths, having your code in place returned a 500 error and did not redirect correctly (redirected to _layouts/error.aspx, in fact).

Is there a step I am missing? I have the web.config changed to have your entry and it is first in the httpModules section. The dll is in the BIN (though how that runs with elevated permissions in the BIN as anonymous I don’t know).

But I cannot get it working - even in order to test it by tracing the redirects.  

Thoughts?

June 26, 2008 1:59 PM
 

Tim Dobrinski said:

Found the problem, well problems.  

First, the part that helps InfoPath and Designer was the first problem.  Commenting that out got me further.  (And SharePoint Designer doesn't seem effected by the exclusion of this).

Second, and most importantly, my question about how you can run Elevated Privlidges in anonymous mode (or even Medium Trust) turned out the big problem.  My site being an anonymous site and run in Medium Trust, this control was not able to run code in Elevated Privlidges from the BIN.  I moved the control in the GAC, but that didn't work.  So I created a custom CAS file and gave the redirect control FullControl.  Works like a champ now.  

Also, I removed the Variations section from your code.  It is a two-line fix in the VariationsRootLanding.ascx file and your code was only for your language and naming styles.  Figuring out how to fit in actual Variations logic in there would ahve taken way too long.

Thanks

Tim

June 26, 2008 4:02 PM
 

Tim Dobrinski said:

I posted my changes to yuor code at my blog if your interested.  I put them there to keep from posting a 5 page comment.

www.thesug.org/.../Post.aspx

June 27, 2008 1:04 PM
 

Waldek Mastykarz said:

Thanks for the code, Tim. While on optimizing the code even further I though of compiling the Regex to improve its performance.

In your post you have presented a simpler way to deal with variations. You advice to do it by modifying the VariationRootLogic.ascx. As it's as SharePoint system file I would definitely discourage anyone from doing that. An update or a bugfix in SharePoint might break the modified functionality by overwriting the file. Instead you could either provide your own page with your own control to include the required logic or extend the Variation logic.

As I've mentioned: from my own experience I know, that customers don't want to fully rely on the language defined in the User Agent. In many cases visitors don't even know about such options. That can lead to displaying the English variation by default for the Dutch visitors who are using English OS. Hardcoding the default variation is not such a bad practice in my opinion.

June 28, 2008 3:58 AM
 

Tim Dobrinski said:

But for my site, which has 6 published languages and 14 more in various states of development, hardcoding variations is not an option.  And since SharePoint and Google use the browser language to route users to the proper language, I have to rely on that as well as it is the closest thing to universal I have.  

As for modifying the VariationRootLogic.ascx file, MIcrosoft gives examples of modifying the logic for your own use, so while not the best practice, even Microsoft leaves it as an option.  Besides, there are times when it's unavoidable.  Just, if you're running a patch or hotfix, just make copies of your custom files so you don't loose them.

Either way, I respect that we have different needs in our solutions.  I apreciate your post as it set me on a good path to a solution that fits my needs.  I thank you for that.

June 30, 2008 9:13 AM

Leave a Comment

(required )  
(optional )
(required )  
Add

About Waldek Mastykarz

Waldek Mastykarz is a Dutch SharePoint 2007 developer specialized in Web Content Management solutions in Microsoft Office SharePoint Server 2007, web standards and accessibility.

Need SharePoint Training? Attend a SharePoint Bootcamp!

Posts (c) their respective authors. Everything else (c) 2007 SharePoint Experts