I'm new to web development, so it's very possible I'm missing something quite obvious here, thank you to anyone for any help.
I'm encountering a Mixed Content error in Chrome when I use hx-post on a form, even when I specify the url to contain https or give the _scheme to the url_for Flask function.
I'm trying to make a simple webpage that allows a user to download an input template, upload the completed template, run a script on the input, and then download the resulting file.
I'm using Flask, the server is Gunicorn, and it's hosted on Azure Web App Services.
Full webpage HTML:
<!doctype html>
<head>
<title>My app</title>
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap/css/bootstrap.min.css') }}">
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
<script src="static/bootstrap/htmx/htmx.2.0.3.min.js"></script>
<script src="static/bootstrap/htmx/hyperscript.min.js"></script>
</head>
<html>
<body>
<main>
<div class="px-4 py-3 my-2 text-center">
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='images/image.jpg') }}"
alt="Logo" width="384" height="192" />
<h1 class="display-6 fw-bold text-primary">Welcome to The App</h1>
</div>
<div class="px-4 py-3 my-2 text-center">
<form method="post" action="{{ url_for('template_download') }}">
<div class="col-md-6 mx-auto">
<label for="name" class="form-label fw-bold fs-3">1. Download the Input Template</label>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center my-2">
<button type="submit" class="btn btn-primary btn-lg px-4 gap-3">Download</button>
</div>
</div>
</form>
</div>
<div class="px-4 py-3 my-2 text-center">
<form hx-encoding='multipart/form-data' hx-post="/input_upload">
<div class="col-md-6 mx-auto">
<label for="input_file" class="form-label fw-bold fs-3">2. Select the Completed Input File</label>
<p><input type='file' name='input_file'></p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center my-2">
<button class="btn btn-primary btn-lg px-4 gap-3">
Upload
</button>
</div>
</div>
</form>
</div>
</main>
</body>
</html>
The specific section causing the issue:
<div class="px-4 py-3 my-2 text-center">
<form hx-encoding='multipart/form-data' hx-post="/input_upload">
<div class="col-md-6 mx-auto">
<label for="input_file" class="form-label fw-bold fs-3">2. Select the Completed Input File</label>
<p><input type='file' name='input_file'></p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center my-2">
<button class="btn btn-primary btn-lg px-4 gap-3">
Upload
</button>
</div>
</div>
</form>
</div>
I've also tried "{{ url_for('input_upload', _scheme='https', _external=True) }}"
in the hx-post field, and the explicit full https url to the page. All produce the Mixed Content error.
I've also tried it without HTMX, and get a 400 error which I don't understand:
<div class="px-4 py-3 my-2 text-center">
<form method="post" action="{{ url_for('input_upload') }}" enctype="multipart/form-data">
<div class="col-md-6 mx-auto">
<label for="file" class="form-label fw-bold fs-3">2. Select the Completed Input File</label>
<p><input type="file" name="file"></p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center my-2">
<button type="submit" class="btn btn-primary btn-lg px-4 gap-3">Upload</button>
</div>
</div>
</form>
</div>
When I inspect the page after attempting an upload, I see the full https url in the hx-post field when using url_for, but the Mixed Content error message shows http:
Mixed Content: The page at 'https://<my_website_name>/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://<my_website_name>/input_upload'. This request has been blocked; the content must be served over HTTPS.
I don't understand what's causing this, so any help is really appreciated.
P.S. here is the app.py for the website as well:
import os
import logging
from werkzeug.utils import secure_filename
from flask import (
Flask,
redirect,
render_template,
request,
send_from_directory,
url_for,
)
# Sets logging level to DEBUG.
logging.basicConfig(filename="record.log", level=logging.DEBUG)
# Start and configure the Flask app.
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 * 10 # 10 MB limit
app.config["UPLOAD_EXTENSIONS"] = [".xlsx", ".xlsm"]
app.config["UPLOAD_PATH"] = "uploads"
app.config["PREFERRED_URL_SCHEME"] = "https"
# Filesize validation. Automatically detected by Flask based on the configuration.
@app.errorhandler(413)
def too_large(e):
return "File is too large", 413
def allowed_file(filename):
return (
"." in filename
and filename.rsplit(".", 1)[1].lower() in app.config["UPLOAD_EXTENSIONS"]
)
# App Page Routing.
@app.route("/")
def index():
app.logger.debug("Request for index page received")
return render_template("index.html")
@app.route("/favicon.ico")
def favicon():
app.logger.debug("Request for favicon received")
return send_from_directory(
os.path.join(app.root_path, "static"),
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
@app.route("/template_download", methods=["POST"])
def template_download():
app.logger.debug("Request for template download received")
return send_from_directory(
os.path.join(app.root_path, "static"),
"<filename>.xlsm",
mimetype="application/vnd.ms-excel.sheet.macroEnabled.12",
)
@app.route("/input_upload", methods=["POST"])
def input_upload():
app.logger.debug("Request for input upload received")
if request.files:
input_file = request.files["input_file"]
if input_file.filename == "":
app.logger.debug("No file selected")
return redirect(request.url)
else:
if allowed_file(input_file.filename):
# Gets the username from the email address in header.
name = request.headers["X-MS-CLIENT-PRINCIPAL-NAME"].split("@")[0]
# Creates a folder for the user if it doesn't exist.
os.makedirs(
os.path.join(app.config["UPLOAD_PATH"], name), exist_ok=True
)
# Saves the file to the user's folder. Always overwrites prior input.
input_file.save(
os.path.join(
app.config["UPLOAD_PATH"], name, "input.xlsx"
) # Always save as .xlsx.
)
app.logger.debug(f"User {name} input saved")
return redirect(request.url)
else:
app.logger.debug("File type not allowed")
return redirect(request.url)
@app.route("/output_download", methods=["POST"])
def output_download():
app.logger.debug("Request for output download received")
pass
if __name__ == "__main__":
app.run(
# debug=True,
)
I tried your code and deployed to the Azure web app without any issues.
The Mixed Content error is due to a conflict between HTTP and HTTPS requests on a site served over HTTPS.
Azure Blob Storage
to store uploaded files because it provides long term storage and easy to access.Https only
in configuration section of Azure Web app.
I made small changes in your app. py then it worked fine for me.
app.config["UPLOAD_EXTENSIONS"] = ["xlsx", "xlsm"]
@app.route("/input_upload", methods=["POST"])
def input_upload():
app.logger.debug("Request for input upload received")
if 'input_file' not in request.files:
app.logger.debug("No file part in the request")
return redirect(url_for('index'))
input_file = request.files["input_file"]
if input_file.filename == "":
app.logger.debug("No file selected")
return redirect(url_for('index'))
if allowed_file(input_file.filename):
name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME", "default_user").split("@")[0]
user_folder = os.path.join(app.config["UPLOAD_PATH"], name)
os.makedirs(user_folder, exist_ok=True)
save_path = os.path.join(user_folder, "input.xlsx")
input_file.save(save_path)
app.logger.debug(f"User {name} input saved at {save_path}")
return redirect(url_for('index'))
else:
app.logger.debug("File type not allowed")
return redirect(url_for('index'))
Below is my complete code of app. py:
import os
import logging
from flask import Flask, redirect, render_template, request, send_from_directory, url_for
logging.basicConfig(filename="record.log", level=logging.DEBUG)
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024 * 10 # 10 MB limit
app.config["UPLOAD_EXTENSIONS"] = ["xlsx", "xlsm"]
app.config["UPLOAD_PATH"] = "uploads"
app.config["PREFERRED_URL_SCHEME"] = "https"
@app.errorhandler(413)
def too_large(e):
return "File is too large", 413
def allowed_file(filename):
return (
"." in filename
and filename.rsplit(".", 1)[1].lower() in app.config["UPLOAD_EXTENSIONS"]
)
@app.route("/")
def index():
app.logger.debug("Request for index page received")
return render_template("index.html")
@app.route("/favicon.ico")
def favicon():
app.logger.debug("Request for favicon received")
return send_from_directory(
os.path.join(app.root_path, "static"),
"favicon.ico",
mimetype="image/vnd.microsoft.icon",
)
@app.route("/template_download", methods=["GET", "POST"])
def template_download():
app.logger.debug("Request for template download received")
return send_from_directory(
os.path.join(app.root_path, "static"),
"template.xlsm", # Replace with your actual file name
mimetype="application/vnd.ms-excel.sheet.macroEnabled.12",
as_attachment=True
)
@app.route("/input_upload", methods=["POST"])
def input_upload():
app.logger.debug("Request for input upload received")
if 'input_file' not in request.files:
app.logger.debug("No file part in the request")
return redirect(url_for('index'))
input_file = request.files["input_file"]
if input_file.filename == "":
app.logger.debug("No file selected")
return redirect(url_for('index'))
if allowed_file(input_file.filename):
name = request.headers.get("X-MS-CLIENT-PRINCIPAL-NAME", "default_user").split("@")[0]
user_folder = os.path.join(app.config["UPLOAD_PATH"], name)
os.makedirs(user_folder, exist_ok=True)
save_path = os.path.join(user_folder, "input.xlsx")
input_file.save(save_path)
app.logger.debug(f"User {name} input saved at {save_path}")
return redirect(url_for('index'))
else:
app.logger.debug("File type not allowed")
return redirect(url_for('index'))
@app.route("/output_download", methods=["POST"])
def output_download():
app.logger.debug("Request for output download received")
pass
if __name__ == "__main__":
app.run(debug=True)
Azure App service Output: