FastAPI with nginx does not serve static files in HTTPS
Asked Answered
T

4

7

I have a small, test FastAPI web application that is serving a simple HTML page that requires a css style sheet located in the static folder. It is installed on a Linode server (Ubuntu 20.04 LTS), nginx, gunicorn, uvicorn workers, and supervisorctl. I have added a certificate using certbot.

The application works fine in http but does not access the static files in https. When accessed in http all static-based features work but when accessed with https it lacks all styling from css stylesheet. I need to get this working so I can load a much more complex app that needs css and other static folder-stored features.

The file structure is:

/home/<user_name>/application
- main.py
- static
   |_ css
   |_ bootstrap
- templates
   |_ index.html

main.py:

import fastapi
import uvicorn
from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates


api = fastapi.FastAPI()

api.mount('/static', StaticFiles(directory='static'), name='static')
templates = Jinja2Templates(directory="templates")


@api.get('/')
@api.get('/index', response_class=HTMLResponse)
def index(request: Request):
    message = None
    return templates.TemplateResponse("index.html", {"request": request,
        'message': message})


if __name__ == '__main__':
    uvicorn.run(api, port=8000, host='127.0.0.1')

nginx is at /etc/nginx/sites-enabled/<my_url>.nginx

server {
    listen 80;
    server_name www.<my_url>.com <my_url>.com;
    server_tokens off;
    charset utf-8;

    location / {
        try_files $uri @yourapplication;
    }

    location /static {
        gzip            on;
        gzip_buffers    8 256k;

        alias /home/<user_name>/application/static;
        expires 365d;
    }


    location @yourapplication {
        gzip            on;
        gzip_buffers    8 256k;

        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Protocol $scheme;
    }
  }

server {
    listen              443 ssl;
    server_name         www.<my_url>.com;
    ssl_certificate /etc/letsencrypt/live/<my_url>.com/fullchain.pem; # mana>
    ssl_certificate_key /etc/letsencrypt/live/<my_url>.com/privkey.pem; # ma>
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    location / {
        try_files $uri @yourapplication;
    }

  location /static {
        gzip            on;
        gzip_buffers    8 256k;

        alias /home/<user_name>/application/static;
        expires 365d;
    }

    location @yourapplication {
        gzip            on;
        gzip_buffers    8 256k;

        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Protocol $scheme;
    }
}

and am serving using supervisor script:

[program:api]
directory=/home/<user_name>/application
command=gunicorn -b 127.0.0.1:8000 -w 4 -k uvicorn.workers.UvicornWorker main:api
environmentenvironment=PYTHONPATH=1
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stderr_logfile=/var/log/app/app.err.log
stdout_logfile=/var/log/app/app.out.log

The css stylesheet is called in the html using url_for like this:

<link href="{{ url_for('static', path='/css/ATB_style.css') }}" rel="stylesheet">

I have tried a whole host of modifications to the location /static block in nginx including:

  • adding slash after static in either line or both
  • trying to add https://static or https://www.<my_url>.com/home/<my_url>/application/static
  • adding and removing the location static from the http and https lines
  • changing proxy_pass to https://127.0.0.1:8000;
  • added root /home/<user_name>/application to the server section

I have loaded this server twice, once letting certbot modify the nginx file the second, and current configuration, where I did it manually. I am at a complete loss on what to do.

Trencher answered 15/10, 2021 at 12:53 Comment(8)
browser will block resoures loaded by HTTP request if you visit a HTTPS URL. make sure your img/css/js URL not hardcoded with http://Apathetic
Everything looks okay. Tries to add a trailing slash in both server https as well as http. Then also change the root directory to your applications directory. Just tries to restart the server. With systemctl. Remember you need to restart both. Nginx and also gunicorn. And please file serving via your fastapi app because nginx is fast in terms on file sharing. After ping me if it still not fixed.Filtration
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> Add this to your html page. If you really have hardcode http urls problems. It will redirect all http requests to https.Filtration
@emptyhua, thanks, I am using url_for, edited the question to show the actual line.Trencher
@AkramKhan, thank you, adding the meta did the trick. WOW!Trencher
@Brad Allen so you have hard coded http route in your website. A better is not to rely on meta tag. Just find the http route in website. And change that to https.Filtration
@AdramKhan, this is really puzzling as I pulled almost everything out, searched through all files for http and all references are https. The only thing I can think of is some sort of call in a python dependency. I'm lost without the meta http-equivalent.Trencher
@AdamKhan, kept troubleshooting as you advised, found root cause and posted edited answer. Thank you for the help.Trencher
T
8

Thanks to @AdramKhan for the comment that provided a work-around for an important demo. I added a meta line to my html page to allow access to the css stylesheet with https:

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

This was only a work-around as it is dealing with a hard-coded HTTP request in the code somewhere per this: How can I allow Mixed contents (http with https) using content-security-policy meta tag?

Solving the root cause was changing how static content was called in the head of html files. The problems (there were three) were with references like this where there was a jinja2 url_for instead of a direct href:

<link href="{{ url_for('static', path='/css/MH_style.css') }}" rel="stylesheet">

When replaced with a reference of this format, using href:

<link rel="stylesheet" href="/static/css/MH_style.css"/>

Everything worked without the Content-Security-Policy meta.

Trencher answered 15/10, 2021 at 14:52 Comment(1)
You, sir, saved my day - spent the better part of a working day hunting for a solution to this problem.Daystar
D
0

I'd think you want the server to handle it. If you just setup a separate block on port 80 to convert all requests to 443 (HTTPS) permanently, you'd be good:

server {
    listen 80;
    server_name yourserver.com;
    return 301 https://yourserver.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourserver.com;
    ...
}```
Dunc answered 15/10, 2021 at 17:19 Comment(3)
Changed the port 80 to a redirect, as in this answer, then commented out the meta http-equivalent line in the html. The redirect works great but lost the static directory access again and was not using the css style sheet. Added the http-equivalent line back and it worked with the redirect. Would love to figure out what I've got different than, apparently, everyone else and causes this problem.Trencher
Is there any chance this is related to using Bootstrap 4.1 from disk (in the static folder as well)?Trencher
Kenndy, I followed your advice and found the direct http call. Edited answer to show result.Trencher
L
0

In case of using upstream, proxy_pass should also be set as follow:

http {
    upstream myapp {
        server application_container:4001;
    }

    server {

        listen 80;
        server_name localhost;

        location / {
            ...
        }

        location /myapp/ {
                proxy_pass http://myapp;
                ...
        }

        location /static/ {
            proxy_pass http://myapp;
            alias ...
        }
}
Landsknecht answered 12/11, 2021 at 15:21 Comment(0)
L
0

A bit late, but I encountered this issue today and fixed it. I'm using nginx as a reverse proxy that queries my server. I originally followed Brad Allen's answer, but while that worked on my production server it stopped running on localhost (as that is sending HTTP requests, not HTTPS). I saw in the docs that I can update the response headers instead of adding the tag into my HTML: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy.

I changed my nginx config on my production server to add a header to the response before it's sent back to the user.

add_header Content-Security-Policy upgrade-insecure-requests;

This cleared up my issue.

I tossed up a few options and settled on this in the end, but if it leads to issues then I'll try one using an environment variable "HTTPS_REQUESTS" which I access when generating my HTML with Jinja2.

Libertylibia answered 25/9 at 6:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.