0

Let's say I've got this mcve:

mcve.py

import textwrap
from pathlib import Path
from flask import Flask


working_dir = Path(__file__).parent
app = Flask(
    __name__,
    # static_folder=str(working_dir / "uploads"),
)


@app.route("/test")
def index():
    return textwrap.dedent(
        """
        <!DOCTYPE html>
        <html>
        <head>
            <title>Hello world</title>
        </head>
        <body>
            <img src="foo/foo.png">
            <img src="bar/bar.png">
        </body>
        </html>
    """
    ).strip()


if __name__ == "__main__":
    with app.test_client() as c:
        print(c.get("/test").data.decode("utf-8"))

run.bat

set FLASK_APP=mcve.py
set FLASK_ENV=development
flask run

If I execute run.bat and then I go to http://localhost:5000/test in the browser I'll get:

>flask run
 * Serving Flask app "x.py" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with windowsapi reloader
 * Debugger is active!
 * Debugger PIN: 497-008-397
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [31/May/2020 22:32:19] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [31/May/2020 22:32:22] "GET /test HTTP/1.1" 200 -
127.0.0.1 - - [31/May/2020 22:32:22] "GET /foo/foo.png HTTP/1.1" 404 -
127.0.0.1 - - [31/May/2020 22:32:22] "GET /bar/bar.png HTTP/1.1" 404 -

Question

How should I modify mcve.py to server the images properly (now you can see is giving 404) in development using flask's server but then serve them properly with nginx on production?

BPL
  • 9,632
  • 9
  • 59
  • 117
  • `` should make use of `url_for` but, otherwise, those lookups will be intercepted by nginx when you set up its configuration file. It would be much easier if you were actually storing in a subdirectory of a directory called `static`, then you configure your nginx to load those. In other words, you don't configure anything on the flask side to take over once you move to nginx – roganjosh May 31 '20 at 20:40

1 Answers1

1

You don't have to configure flask specifically for this. These logs:

127.0.0.1 - - [31/May/2020 22:32:19] "GET / HTTP/1.1" 404 -
127.0.0.1 - - [31/May/2020 22:32:22] "GET /test HTTP/1.1" 200 -
127.0.0.1 - - [31/May/2020 22:32:22] "GET /foo/foo.png HTTP/1.1" 404 -
127.0.0.1 - - [31/May/2020 22:32:22] "GET /bar/bar.png HTTP/1.1" 404 -

are actually generated by the werkzeug development server that's serving the static content for you. When you move to using nginx, you can intercept the GET requests with a URL rule. From the example nginx config file from the Flask Mega Tutorial:

...

server {

    ...

    location / {
        # forward application requests to the gunicorn server
        proxy_pass http://localhost:8000;
        proxy_redirect off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location /static {
        # handle static files directly, without forwarding to the application
        alias /home/ubuntu/microblog/app/static;
        expires 30d;
    }
}

Note the /location rule that handles requests to static directly, so those requests from the browser wouldn't even hit whatever is serving your flask app in production. There are many more methods for matching urls that you can find, e.g. here. It's perfectly possible to add multiple such rules to look in separate locations but you'll want to structure the app to give these files a distinct location so you can implement such rules.


Separately, to get around your current 404s, look at the template docs for Jinja2 and use the url_for method to make sure it resolves relative paths correctly.

For example, if I want to include:

<link href='static/css/bootstrap-4.3.1.min.css' rel="stylesheet">

I would instead use:

<link href="{{ url_for('static', filename='css/bootstrap-4.3.1.min.css') }}" rel="stylesheet">

This passes off the responsibility of path resolution to the app, so no matter how many hyperlinks I've followed or blueprints I'm using, this path will always resolve to the correct static directory when the template is rendered.

I'm not sure this will work with return textwrap.dedent because it might not invoke Jinja on the template. You can import render_template_string for a throwaway example like you have here, but render_template will work in your actual app too.

roganjosh
  • 12,594
  • 4
  • 29
  • 46
  • The part where you talk about nginx is clear and nice, I think i've understood that part... what it's still confusing me is the `Separately, to get around your current 404s, look at the template docs for Jinja2 and use the url_for method to make sure it resolves relative paths correctly.` one... if you could put an example of how you'd adjust the snippet i've posted that'd be helpful indeed, +1 for the nginx part though. – BPL May 31 '20 at 21:07
  • Sure, it's a bit clearer... but you're still sticking to the handled by default `static` route. As you can see in my example i've got 2 folders called `foo` and `bar` that contain images... if you could adjust your explanation to that particular example i could validate straightaway – BPL May 31 '20 at 21:33
  • @BPL you haven't given the app structure. You'll want to look at something [like this](https://stackoverflow.com/a/26972238/4799172) because we're going outside of the topic of the question here into just rendering any media file from any location. Why can't you just put them in a subdirectory of `static` called `uploaded_images` or something and you could verify by using the same method I did for `css`? – roganjosh May 31 '20 at 21:37
  • @BPL or just `foo` and `bar` as subdirectories. I'd have to mock up a full app example on my end to test whether it will work successfully in all cases outside of `static` as the parent folder, which I think is a bit extreme when you already have that. If `foo` or `bar` are on the same level as `static`, then just modify the current path example to point to that parent directory and then give a `filename` that is a relative path from that point – roganjosh May 31 '20 at 21:42
  • Alright, it'd have been nice to have an answer referring to the provided snippet but i guess you've provided more than good enough content to become validated. Plus... I know if i don't validate now this thread will go to the ether and it'll be forgotten forever ;) . Thank you very much – BPL May 31 '20 at 22:04
  • 1
    @BPL you're welcome. I hope I didn't seem too obstinate; we'd be here all night and I hope I gave you enough links and thoughts to move on with. Best of luck! – roganjosh May 31 '20 at 22:05