pythonflaskazure-web-app-servicehtmx

Why is HTMX's hx-post on a form element causing a Mixed Content Error?


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,
    )

Solution

  • 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.

    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:

    enter image description here