Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird `/` behavior when Serving Flask with Apache2 + Gunicorn

I'm trying to build multiple endpoints and subendpoints within my application, part of it as a learning exercise, and part of it is that I have 2 domains. For simplicity I'm going to refer to them as domain1 and domain2.

My Flask listening endpoints are on /api1 and /api2 for domains 1 & 2 respectively. Gunicorn is bound to listen on a unix socket at sock/domain1.sock and sock/domain2.sock. So far everything is working this way.

My Apache2 proxies the endpoints into the proper socket as follows: for domain1 I have:

<Location /api>
    ProxyPass unix:/var/www/socks/domain1.sock|http://127.0.0.1/api1
    ProxyPassReverse unix:/var/www/socks/domain1.sock|http://127.0.0.1/api1 
</Location>

for domain2 I have:

<Location /api>
    ProxyPass unix:/var/www/socks/domain2.sock|http://127.0.0.1/api2
    ProxyPassReverse unix:/var/www/socks/domain2.sock|http://127.0.0.1/api2
</Location>

I know that I don't need to have 2 sockets, but I'm doing so just for testing.

Now when I open domain1.com/api things are working perfectly. And so are for domain2.com/api But when I open domain1.com/api/ (with a slash at the end) or domain2.com/api/ then it gives me a Site Not Found error. This is understandable since in my Flask I'm listening to the endpoint without a trailing slash. The fix for that is to implement / into my flask endpoint. So when I do that, the weird behavior occurs.

New Flask listening Endpoints are /api1/ and /api2/ (with trailing slash). Now when I open domain.com/api/ it is working as intended. But when I'm on domain.com/api (without the slash) it's referring me to either domain.comapi or 127.0.01/api, where both are wrong scenarios. I tried to add a trailing slash in my Apache config, and tried multiple Flask approaches but they're all doing the same weird behavior and I can't understand why it's doing that. Now personally I don't mind using the endpoint without the slash, I just want to understand why this is happening. I also tried googling a lot but nothing came up related to my query.

Reproduceable Behavior: I'm unable to link the 2nd domain as it is a protected IP for my company, so I created multiple endpoints so that you can click on to simulate the behavior.

  • https://thethiny.xyz/api1 -> sock|http://127.0.0.1/api1 -> internal /api1
  • https://thethiny.xyz/api2 -> sock|http://127.0.0.1/api2 -> internal /api2/
  • https://thethiny.xyz/api3 -> sock|http://127.0.0.1/api1/ -> internal /api1
  • https://thethiny.xyz/api4 -> sock|http://127.0.0.1/api2/ -> internal /api2

Working:

  • https://thethiny.xyz/api1
  • https://thethiny.xyz/api2/
  • https://thethiny.xyz/api4

Not Found:

  • https://thethiny.xyz/api1/
  • https://thethiny.xyz/api3
  • https://thethiny.xyz/api3/

Weird Redirect:

  • https://thethiny.xyz/api2
  • https://thethiny.xyz/api4/

Edit: I understand the problem and have came up with some solutions in the answer below. I'm not satisfied with the solutions but I'm taking this as a limitation of mapping endpoints to different underlying endpoints. For more information, read about Reverse Proxy Pass and Redirects and Rewriting Location Header in HTTPd

like image 773
thethiny Avatar asked Jun 10 '26 03:06

thethiny


1 Answers

I now understand the problem. So in my Apache Proxy it is giving the request to Flask on the endpoint specified 127.0.0.1/api2, so when there's a redirect request from within Flask, it tries to redirect to 127.0.0.1/api2/, since Flask doesn't have any information about the original url source. Using ProxyPreserveHost solves this only when the endpoint resources match, as in mapping /api2/ to /api2 but not /api4/ to /api2/, since on the redirect, Flask receives a request for /api2 -> /api2/ and returns that having no information about /api4. Unfortunately I don't there's an actual solution to this from Apache2/Flask configurations other than manually handling the routes specifically to how you want them to be, as in do not allow Flask to redirect automatically since it will not know how, and instead either manually redirect (external redirect) to the correct endpoint, or handle each route separately (/api and /api/stuff but not /api/).

Example:

app.add_url_rule("/api2", view_func=StubFunction(), redirect_to="/api2/")
app.add_url_rule("/api2/", view_func=ActualFunction())

And add ProxyPreserveHost On to your Apache2 config or use the built in Proxy Fixer if you don't want to modify your Virtual Hosts:

from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

What happens now is that 127.0.0.1 gets translated to yourdomain.tld when delivered to your Flask app. So when you're redirecting back using redirect_to, you're redirecting to your domain externally, no longer relatively. So in the case above, /api2 is redirecting to myDomain.tld/api2/ then /api2/ is called, which is functional. You can also skip the preserve host and manually put in your domain name in the redirect as so:

app.add_url_rule("/api2", view_func=StubFunction(), redirect_to="https://yourDomain.tld/api2/")

But I don't like this approach in case you change your domain for some reason.

tl;dr, don't put a trailing slash in your ProxyPass Applications.

like image 86
thethiny Avatar answered Jun 13 '26 08:06

thethiny