TheBlueSky
TheBlueSky

Reputation: 5948

Handling URL's with AppHarbor without Modifying All My Controllers

I'm trying to host an MVC 3 application (FunnelWeb) on AppHarbor. For a reason that's still not clear to me, when my route is only a Controller+Action (e.g. mysite/admin is Admin+Index and mysite/login is Admin+login) everything works fine, but if I have anything else in the route (e.g. a variable like {*page}) my URL will be mysite:12345/mypage (where 12345 is a port number assigned by AppHarbor and mypage is the name of the page I'm requesting). This makes the request fail as the port 12345 is not publicly exposed.

AppHarbor uses load balancing to distribute the request between multiple IIS's. This is their way of doing stuff and this is why internally the requests are routed to some non-standard ports. I don't have a problem with that, but I have problem with MVC that tries to route me to that internal URL.

I'm not pointing fingers here; it's nobody's fault :) so let's move to the question:

  1. Why there is a difference between requesting a route with Controller+Action only and requesting a route with a variable like {*page}? Be technical please :)
  2. Here is an example of how to handle requests in AppHarbor, however, it seems that it requires me to modify all my controllers (OMG). Is there any way to implement this without modifying my controllers?
  3. Any other suggestions are welcomed :)

Thanks in advance.

UPDATE: Coincidentally, the behaviour that I observed matches the conclusion that I reached. However, the issue has nothing to do with ASP.Net MVC routing. The short story is, FunnelWeb forces lowercase URL's, so, whenever it receives a request to a resource it convert it to lowercase, if needed, and issue a 301 response. The problem is, when creating the URL for the 301 response, the request URL (absolute URL) is now the URL used when the request made from the load balancer to IIS and not the one made from the client; hence, the request fails.

Upvotes: 3

Views: 1177

Answers (3)

ferventcoder
ferventcoder

Reputation: 12561

It's possible that the following is now all you need:

<appSettings>
  <!-- AppHarbor Setting to stop AppHb load balancer internal port numbers from showing up in URLs-->
  <add key="aspnet:UseHostHeaderForRequestUrl" value="true" />
</appSettings>

This is noted as an update on AppHarbor's support page at http://support.appharbor.com/kb/getting-started/workaround-for-generating-absolute-urls-without-port-number

MSDN says the following about UseHostHeaderForRequestUrl:

aspnet:UseHostHeaderForRequestUrl - If this value attribute is false [default], the Url property is dynamically built from the host, port, and path provided by the web server. If this value attribute is true, the Url property is dynamically built by using the host and port provided by the incoming "Host" header and the path provided by the web server.

Upvotes: 3

Troels Thomsen
Troels Thomsen

Reputation: 11617

There is a way, but it requires a couple of classes.

When ASP.NET MVC registers a route, it defines a route handler. This route handler returns a HTTP handler that handles the request. If you use a custom route handler that returns a custom HTTP handler, you can rewrite the HTTP context by using a couple decorator classes.

Start by creating a HttpContextProxy and HttpRequestProxy that derives from the base classes and wraps all methods and properties to an inner instance. I've made the hard work available.

Next create the decorators, first the HTTP context decorator:

using System.Web;

public class HttpContextDecorator : HttpContextProxy
{
    public HttpContextDecorator(HttpContextBase innerHttpContext)
        : base(innerHttpContext)
    {
    }

    public override HttpRequestBase Request
    {
        get
        {
            return new HttpRequestDecorator(base.Request);
        }
    }
}

The HTTP request decorator:

using System;
using System.Web;

public class HttpRequestDecorator : HttpRequestProxy
{
    public HttpRequestDecorator(HttpRequestBase innerHttpRequest)
        : base(innerHttpRequest)
    {
    }

    public override bool IsSecureConnection
    {
        get
        {
            return string.Equals(Headers["X-Forwarded-Proto"], "https", StringComparison.OrdinalIgnoreCase);
        }
    }

    public override Uri Url
    {
        get
        {
            var url = base.Url;
            var urlBuilder = new UriBuilder(url);

            if (IsSecureConnection)
            {
                urlBuilder.Port = 443;
                urlBuilder.Scheme = "https";
            }
            else
            {
                urlBuilder.Port = 80;
            }

            return urlBuilder.Uri;
        }
    }

    public override string UserHostAddress
    {
        get
        {
            const string forwardedForHeader = "HTTP_X_FORWARDED_FOR";
            var forwardedFor = ServerVariables[forwardedForHeader];
            if (forwardedFor != null)
            {
                return forwardedFor;
            }

            return base.UserHostAddress;
        }
    }
}

As mentioned, you also need to override the MVC classes - here the HTTP handler:

using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

public class CustomMvcHandler : MvcHandler
{
    public CustomMvcHandler(RequestContext requestContext)
        : base(requestContext)
    {
        requestContext.HttpContext = new HttpContextDecorator(requestContext.HttpContext);
    }

    protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
    {
        httpContext = new HttpContextDecorator(httpContext);
        return base.BeginProcessRequest(httpContext, callback, state);
    }

    protected override void ProcessRequest(HttpContextBase httpContext)
    {
        httpContext = new HttpContextDecorator(httpContext);
        base.ProcessRequest(httpContext);
    }
}

Then the route handler:

using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

public class CustomMvcRouteHandler : MvcRouteHandler
{
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new CustomMvcHandler(requestContext);
    }
}

Finally, you'll need to replace the associated handler for all registered routes (or map them properly from the beginning):

var routes = RouteTable.Routes.OfType<Route>().Where(x => x.RouteHandler is MvcRouteHandler);
foreach (var route in routes)
{
    route.RouteHandler = new CustomMvcRouteHandler();
}

Upvotes: 2

friism
friism

Reputation: 19279

This is known issue with FunnelWeb url generation on AppHarbor. When using standard MVC methods to generate relative URLs, this is not a problem. AppHarbor has a short guide and sample on how the generate public URLs in the knowledge base.

Upvotes: 3

Related Questions