23

I am having problems proxying an ASP.NET Core MVC app.

My app is running on Kestrel on localhost:5000 and my Apache 2.4 reverse proxy is running on localhost:80. I want to proxy all requests from localhost:80/test to localhost:5000

Here is the httpd.conf part for the proxy:

...
<Location "/test">
   ProxyPass "http://localhost:5000"
   ProxyPassReverse "http://localhost:5000"
</Location>
...

The proxy works, however all links are broken. Like if I have an anchor that links to a controller named HomeController with the action About, the link returned from the proxy is localhost/Home/About instead of localhost/test/Home/About. The host is correct, it's the context root test that is missing.

What is the best practice to handle this? Is it a configuration in ASP.NET Core to specify the context root so that TagHelpers will take it into account? Or is it a configuration in Apache ProxyPass to rewrite links (really not a big fan of this)?

poke
  • 369,085
  • 72
  • 557
  • 602
CurlyFire
  • 712
  • 2
  • 6
  • 18

3 Answers3

49

The problem is that the web application does not know about the subpath /test, so in your case, it will just respond as if it was called directly at the root path.

ASP.NET Core has a concept of a PathBase to remedy this. The PathBase is essentially a part of the request’s URI path that will be considered as a fixed part of the path. Whenever a component within the framework generates an URL, it will take the current PathBase into account and make sure to include that as a prefix to all generated paths.

By default, the PathBase will be empty, and it depends on the configuration of your reverse proxy to say how you should set up the PathBase.

UsePathBaseMiddleware

There is the built-in UsePathBaseMiddleware which can be used to temporarily configure the PathBase for an incoming request. The way this middleware works is basically that it will check whether the request starts with an accepted path prefix and if it does, that prefix will be moved from the Path into the PathBase.

You can activate this using the UsePathBaseExtensions.UsePathBase extension method. Just call the method as the very first thing in your Startup’s Configure method with the base path you want to use:

public void Configure(IApplicationBuilder app)
{
    app.UsePathBase("/test");

    // all the other middlewares
    app.UseStaticFiles();
    app.UseMvc();
    // …
}

Since the middleware will only adjust the PathBase when it sees the /test prefix within the path of incoming requests, you need to make sure that the reverse proxy actually includes that prefix. So you would have to adjust your Apache configuration to include that path:

<Location "/test">
   ProxyPass "http://localhost:5000/test"
   ProxyPassReverse "http://localhost:5000/test"
</Location>

Note that the UsePathBaseMiddleware will not prevent the application from working without that prefix. So you can actually use it both with and without the base path, and it will correctly adapt.

Custom middleware

If you do not want to adjust your reverse proxy configuration to include the path within the forwarded request, then you won’t be able to use the UsePathBaseMiddleware. Instead, you will have to add your own very simple middleware there:

public void Configure(IApplicationBuilder app)
{
    app.Use((context, next) =>
    {
        context.Request.PathBase = "/test";
        return next();
    });

    // all the other middlewares
    app.UseStaticFiles();
    app.UseMvc();
    // …
}

This will set /test as a constant PathBase for incoming requests. So the actually incoming request does not have to include it, which means you can leave your reverse proxy configuration as it is (and not forward the /test path there). However, that also means that unlike with the UsePathBaseMiddleware where the PathBase was set dynamically depending on the incoming request, now all requests to the application will require that path, regardless of whether they go through the reverse proxy or not.

poke
  • 369,085
  • 72
  • 557
  • 602
  • That's exactly what I was looking for. I was also able to find the issue discussed on github based on the method name. https://github.com/aspnet/Hosting/issues/815 – CurlyFire Jul 26 '17 at 13:14
  • Is this still the way to do this? I'm having the same problem, but the `UsePathBase` middleware doesn't seem to change anything... ? – kspearrin Mar 24 '18 at 03:56
  • 1
    @kspearrin Yes, this is still the way to go. If you enable the path base, then your application should be available at `https://example.com/` and `https://example.com/test/`. – poke Mar 24 '18 at 11:34
  • The app is running at ‘/‘ but I am reverse proxying to it at ‘/app’. It seems like it always thinks it’s still running as ‘/‘. – kspearrin Mar 24 '18 at 11:40
  • Does it work if you open the site directly, instead of going through the reverse proxy? If it works that way, then your reverse proxy might be misconfigured. – poke Mar 24 '18 at 11:46
  • UsePathBase doesn't actually work for the original user's question. It only works if the proxy does not trim the base out of the path. https://github.com/aspnet/HttpAbstractions/blob/49b447d6265f0de44304b1b887cbdd3227cb038d/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs#L54 – Tratcher Mar 24 '18 at 14:03
  • @kspearrin your description sounds backwards from a common setup. Are you saying the reverse proxy is forwarding requests from / to kestrel at /app? – Tratcher Mar 24 '18 at 14:06
  • @Tratcher Yeah, you’re right there. Guess I didn’t pay enough attention back when I wrote this answer. I’ve expanded it now to cover the different cases. – poke Mar 24 '18 at 20:19
  • I got this working finally. Turns out `UsePathBase` works correctly, my Nginx proxy was just configured wrong (as you pointed out). Trailing slashes on Nginx locations and proxy_pass will not work. It should look like: `location /app { proxy_pass http://app:5000; }`, and **NOT** `location /app/ { proxy_pass http://app:5000/; }` – kspearrin Mar 25 '18 at 04:13
  • I thought that there was a way of signaling from a proxy to the web application by means of HTTP headers in order to achieve the same effect. Anyone knows about this? – rominator007 Jun 07 '19 at 21:34
  • @rominator007 There’s the ForwardedHeaders middleware which you can enable using [`UseForwardedHeaders`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.forwardedheadersextensions.useforwardedheaders?view=aspnetcore-2.2) but that basically only allow you to adjust the host name. There is no standardized header for a dynamic “path base” configuration (you can easily add this as part of a request header too though and set that in a custom middleware). – poke Jun 07 '19 at 22:21
  • @poke: thx for the idea. But since I do not want to use custom HTTP headers I am now using the UsePathBase-approach – rominator007 Jun 27 '19 at 07:34
  • 2
    This post contains more valuable knowledge than five github.com/dotnet issues and a dozen of official docs I've read during the whole day. Thanks man. – Marc Wittke Mar 03 '22 at 23:55
2

The proxy is dropping request path information so you must re-introduce it by doing something like this:

app.Use((context, next) =>
{
  context.Request.PathBase = "/test";
  return next();
});

This is different from what UsePathBase does, it moves path segments that are still there from the start of Path to the end of PathBase.

https://github.com/aspnet/HttpAbstractions/blob/49b447d6265f0de44304b1b887cbdd3227cb038d/src/Microsoft.AspNetCore.Http.Abstractions/Extensions/UsePathBaseMiddleware.cs#L54

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
Tratcher
  • 5,929
  • 34
  • 44
  • 1
    Doc writeup for this scenario: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-3.1#deal-with-path-base-and-proxies-that-change-the-request-path – Tratcher May 05 '20 at 22:37
2

for .net core you could add this

app.UsePathBase(basePath);
app.UseRouting();
ahaliav fox
  • 2,217
  • 22
  • 21