Short version: I'm trying to make an Electron app with Flask as the backend. Part of what I'm trying to do involves getting a session variable or initializing it if it doesn't exist yet. This does not work, as the session variable isn't recognized even when it is initialized, causing new variables to be created every time the route is called. I suspect this is a problem with my CORS configuration or lack thereof, but I don't know what's wrong or how to fix it. Thus this post.
Long version: My app takes the content of a form and passes it to the backend, which adds it as a node which is then appended to a linked list, which can then be accessed later. I want one of the fields of the node, detail, to be unique. And, to do so, I want to make it so that the user cannot submit a form where the value of "detail" already exists. I am attempting to achieve this with the following JS code:
/* INPUT VALIDATION */
async function checkUniqueDetail(detail_to_check){
try{
const response=await fetch(`${SERVER_URL}/check_detail/${detail_to_check}`,{
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
const data=await response.json();
const existing_details=data.detail_list;
console.log("Existing details:", existing_details);
console.log("Checking detail:", detail_to_check);
const result=data.result;
console.log("Result is:",result);
if(result=="True"){
return false;
}else{
console.log("The detail is unique");
return true;
}
}catch(e){
console.error(e);
return false;
}
}
/* FORM SUBMISSION */
function confirmFormSubmit(formData){
if(confirm("Are you sure you want to submit?")){
const qd=formData.get("detail");
console.log("q_detail is:",qd);
/* Form validation */
checkUniqueDetail(qd).then(result=>{
isUniqueDetail=result;
console.log(qd,"unique?:",isUniqueDetail);
/* If validated, send info */
if(isUniqueDetail==true){
sendForm();
sendDetail(qd);
message.innerText="Question added!"
}else{
message.innerText="A question with this detail already exists. Please try again"
}
});
}
}
form.addEventListener("submit", async (event)=>{
event.preventDefault();
const fData = new FormData(form);
confirmFormSubmit(fData);
});
This calls to the routes made by the Flask app that look like this:
# ROUTES
from flask import Flask, request, session
from flask_session import Session
from flask_cors import CORS
import redis, json
#for forcibly clearing session files
import atexit
app = Flask(__name__)
# Configurations
app.config["SECRET_KEY"]="change_later"
app.config["SESSION_TYPE"] = "redis"
app.config["SESSION_PERMANENT"] = False
r = redis.from_url('redis://127.0.0.1:6379')
app.config['SESSION_REDIS'] = r
def test_redis_connection(redis_session):
"""Check that Redis is connected to"""
try:
redis_session.ping() # Check if Redis is alive
print("Redis connection successful!")
except redis.exceptions.ConnectionError as e:
print(f"Redis connection error: {e}")
exit() # Or handle the error appropriately
test_redis_connection(r)
app.config["CORS_HEADERS"] = "Content-Type"
# Initialize Plugins
sess=Session()
sess.init_app(app)
CORS(app,resources={r"/*": {"origins": "http://localhost*"}},)
#check that the server's running and connected
@app.route("/", methods=["GET","POST"])
def check():
return {"result":"Server active!"}
### ERROR!!!: This only works as a Flask app
# It seems like every time this is run,
# it makes a new session variable, rather than getting the old one
@app.route("/add_detail/<detail>",methods=["GET","POST"])
def add_detail_to_list(detail):
"""Add a q_detail to a list of q_details"""
# Initialize the list if it doesn't exist
if 'lst' not in session:
print("Session variable not found. Initializing...")
session['lst'] = json.dumps([])
session.modified = True
# Append to the list
lst:list[str]=json.loads(session['lst'])
print("Before appending:",lst)
lst.append(detail)
print("After appending:",lst)
session['lst'] = json.dumps(lst)
session.modified = True
return {"response":f"{lst}"}
@app.route("/get_all_details",methods=["GET"])
def get_all_details():
details:list[str]=session.get("lst",[])
return {"result":details}
@app.route("/check_detail/<detail>")
def check_detail(detail):
details:list[str]=session.get("lst",[])
if detail in details:
return {"result":"True","detail_list":details}
else:
return {"result":"False","detail_list":details}
## NOTE: Make sure this works in production
### ERROR!!! Doesn't work with Electron app
# Maybe due to how the Flask app is killed?
def clear_redis_sessions(redis_session):
"""Clears all session data from Redis."""
try:
for key in redis_session.keys("session:*"): # Important: Use a pattern to only delete session keys
redis_session.delete(key)
print("Redis sessions cleared.")
except Exception as e:
print(f"Error clearing Redis sessions: {e}")
atexit.register(clear_redis_sessions,redis_session=r) # Register the cleanup function
# print(app.url_map)
if __name__ == "__main__":
app.run(debug=True)
It says it in the comments, but the error occurs with the "/add_detail/" route, specifically in the initial if statement. It triggers every time, creating a new session["lst"] variable rather than getting the old one. This means that the list the frontend gets is always empty, so the detail is always marked as unique and the form is always allowed to be sent.
Here's a screenshot of the Electron app, showing this happening:
And a screenshot of my redis-cli showing the two session keys that were created:
I suspect that the reason this is happening is due to the nature of cross-site requests. This is because this problem doesn't occur if I run the Flask app independently:
but does if I use curl:
I have CORS enabled, but I suspect I don't have it configured properly. But maybe it also has to do with how I'm connecting to the Flask app. Here's my main.js just in case:
const { app, BrowserWindow } = require('electron');
const { exec } = require("child_process");
/**
* Connects to the Flask app
*/
const connectToFlask=function(){
let python;
//test version
python = require('child_process').spawn('py', ['./py/test_routes.py']);
//executable version
//python = require('child_process').execFile("routes.exe");
python.stdout.on('data', function (data) {
console.log("FLASK RUNNING! data:", data.toString('utf8'));
});
python.stderr.on('data', (data) => { // when error
console.error(`stderr: ${data}`);
console.log(`stderr: ${data}`);
});
python.on("close", (code)=>{
console.log(`child process exited with code ${code}`);
});
}
/**
* Create a new BrowserWindow that's connected to Flask with index.html as its UI
*/
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
});
connectToFlask();
win.loadFile('index.html');
}
/**
* When all the windows close, quit the app
*/
app.on('window-all-closed', () => {
if (process.platform !== 'darwin'){
/* Kill the Flask app on close too */
exec("taskkill /f /t /im python.exe", (err, stdout, stderr) => {
if (err) {
console.error(err);
return;
}
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
console.log('Flask process terminated');
});
app.quit();
}
});
/**
* Wait for the app to be ready, then create a new window
*/
app.whenReady().then(() => {
createWindow();
});
Does anyone know what I'm doing wrong and how to fix it?
Thank you in advance
Okay, I found the solution. Not only did I need to allow requests with cookies from both sides like Detlef said, but I also needed to add:
app.config["SESSION_COOKIE_SAMESITE"]="None"
app.config["SESSION_COOKIE_SECURE"]=True
to the configurations.
So the full Flask app looks like:
# ROUTES
from flask import Flask, request, session
from flask_session import Session
from flask_cors import CORS, cross_origin
import redis, json
app = Flask(__name__)
# Configurations
app.config["SECRET_KEY"]="change_later"
app.config["SESSION_TYPE"] = "redis"
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_COOKIE_SAMESITE"]="None"
app.config["SESSION_COOKIE_SECURE"]=True
r = redis.from_url('redis://127.0.0.1:6379')
app.config['SESSION_REDIS'] = r
def test_redis_connection(redis_session):
"""Check that Redis is connected to"""
try:
redis_session.ping() # Check if Redis is alive
print("Redis connection successful!")
except redis.exceptions.ConnectionError as e:
print(f"Redis connection error: {e}")
exit() # Or handle the error appropriately
test_redis_connection(r)
app.config["CORS_HEADERS"] = "Content-Type"
# Initialize Plugins
sess=Session()
sess.init_app(app)
CORS(app,supports_credentials=True)
#check that the server's running and connected
@app.route("/", methods=["GET","POST"])
def check():
return {"result":"Server active!"}
@app.route("/add_detail/<detail>",methods=["GET","POST"])
def add_detail_to_list(detail):
"""Add a q_detail to a list of q_details"""
# Initialize the list if it doesn't exist
if 'lst' not in session:
print("Session variable not found. Initializing...")
session['lst'] = json.dumps([])
session.modified = True
# Append to the list
lst:list[str]=json.loads(session['lst'])
print("Before appending:",lst)
lst.append(detail)
print("After appending:",lst)
session['lst'] = json.dumps(lst)
session.modified = True
return {"response":f"{lst}"}
@app.route("/get_all_details",methods=["GET"])
def get_all_details():
details:list[str]=session.get("lst",[])
return {"result":details}
@app.route("/check_detail/<detail>")
def check_detail(detail):
details:list[str]=session.get("lst",[])
if detail in details:
return {"result":"True","detail_list":details}
else:
return {"result":"False","detail_list":details}
# print(app.url_map)
if __name__ == "__main__":
app.run(debug=True)
And the form looks like:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>Test Form</title>
<style>
label{
display: block;
}
</style>
</head>
<body>
<h1>Test Form</h1>
<p id="message"></p>
<form id="form">
<label>* Question:<input type="text" name="question" id="question" placeholder="Please enter the JOB NAME" required /></label>
<label>* Detail:<input type="text" name="detail" id="detail" placeholder="Job Name" required /></label>
<button type="submit" id="submit">Continue</button>
</form>
<script>
const SERVER_URL = "http://127.0.0.1:5000";
const form=document.getElementById("form");
const message=document.getElementById("message");
/* INPUT VALIDATION */
async function checkUniqueDetail(detail_to_check){
try{
const response=await fetch(`${SERVER_URL}/check_detail/${detail_to_check}`,{
method: "GET",
credentials:"include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
const data=await response.json();
const existing_details=data.detail_list;
console.log("Existing details:", existing_details);
console.log("Checking detail:", detail_to_check);
const result=data.result;
console.log("Result is:",result);
if(result=="True"){
return false;
}else{
console.log("The detail is unique");
return true;
}
}catch(e){
console.error(e);
return false;
}
}
/* FORM SUBMISSION */
async function sendForm() {
console.log("Reached node creation");
}
async function sendDetail(detail) {
try{
const response=await fetch(`${SERVER_URL}/add_detail/${detail}`,{
method: "POST",
credentials:"include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
}
});
const data=await response.json();
const details=data.response;
console.log("Added detail. Details are now:", details);
}catch(e){
console.error(e);
}
}
/*
Confirm the user wants to submit the form. If so, run form validation and submission
*/
function confirmFormSubmit(formData){
if(confirm("Are you sure you want to submit?")){
const qd=formData.get("detail");
console.log("q_detail is:",qd);
/* Form validation */
checkUniqueDetail(qd).then(result=>{
isUniqueDetail=result;
console.log(qd,"unique?:",isUniqueDetail);
/* If validated, send info */
if(isUniqueDetail==true){
sendForm();
sendDetail(qd);
message.innerText="Question added!"
}else{
message.innerText="A question with this detail already exists. Please try again"
}
});
}
}
form.addEventListener("submit", async (event)=>{
event.preventDefault();
const fData = new FormData(form);
confirmFormSubmit(fData);
});
</script>
</body>
</html>
Thanks everyone for your help!