Flask Session Not Persisting (Postman works, Javascript doesn't)

The caveats section of the fetch documentation says:

By default, fetch won't send or receive any cookies from the server, resulting in unauthenticated requests if the site relies on maintaining a user session.

It is recommended to use AJAX to exchange information with Flask views.

Meanwhile, in your code for the Flask app, the session object is a dictionary. Now, if you access a dictionary with its key session['hello'] and if this key does not exist, a Keyerror is raised. To get around this error, you can use the get() method for dictionaries.

What is happening is: the fetch request does not find the hello key(or GET the session value from the Flask view) in the Flask session.

user = session.get('hello')
return jsonify(session_info=user)

But this will still give you a null value for the session { session_info: null }. Why is that so?

When you send GET/POST requests to the Flask server, the session is initialized and queried from within Flask. However, when you send a Javascript fetch POST request, you must first GET the session value from Flask and then send it as a POST request to your Flask view which returns the session information.

In your code, when the POST request is triggered from fetch, when I send the payload data to Flask, it is received correctly and you check this using request.get_json() in the Flask view:

@app.route('/update', methods=['POST'])
def update():
  user = session.get('hello')
  payload = request.get_json()
  return jsonify(session_info=user, payload=payload)

This will return { payload: 'arbitrary_string', session_info: null }. This also shows that fetch does not receive the session information because we did not call GET first to get the session information from Flask.

Remember: The Flask session lives on the Flask server. To send/receive information through Javascript you must make individual calls unless there is a provision to store session cookies.

const fetch = require('node-fetch');

var url_get = 'http://my_server_ip';
var url_post = 'http://my_server_ip/update';
fetch(url_get, {
  method:'GET'
}).then((response)=>response.json()).then((data) =>fetch(url_post, {
  method: 'POST',
  body: JSON.stringify(data),
  dataType:'json',
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => response.json())
.then((postdata) => {
  console.log(postdata);
}));

The Flask views will change slightly:

@app.route('/', methods=['GET'])
def set_session():
    session['hello'] = 'world'
    return jsonify(session['hello'])

@app.route('/update', methods=['POST'])
def update():
    payload = request.get_json()
    return jsonify(session_info=payload)

When you trigger the Javacript request now, the output will be: { session_info: 'world' }


After a few hours of testing, I managed to figure out the issue. Although I think @amanb's answer highlights the problem, I'm going to answer my own question because what I found is ultimately a simpler solution.

In order to make the POST request return the expected value, I simply needed to add a credentials: 'same-origin' line to the fetch body. This looks like the following:

var url = 'http://my_server_ip/update';
fetch(url, {
  method: 'POST',
  body: JSON.stringify('arbitrary_string'),
  credentials: 'same-origin',   // this line has been added
  headers: {
    'Content-Type': 'application/json'
  }
})
.then(response => response.json())
.then((data) => {
  console.log(data);
})

According to Mozilla's Fetch usage guide,

By default, fetch won't send or receive any cookies from the server, resulting in unauthenticated requests if the site relies on maintaining a user session.

So it seems I looked over this. Changing the credentials to allow communication of the cookie/session between client and server resolved the issue.