2

Environment:

  • Python 3.6.1
  • Flask 0.12.2
  • Werkzeug 0.14.1

While writing tests for my Flask application, I have discovered the following peculiarity: if in tests for Flask application you will be redirected to the same url two times 'in a row' ClientRedirectError: loop detected will be thrown, even if it stops redirecting after the second redirect (i.e. loop is not, actually, happening).

Consider the following, simplified, example:

app.py

from flask import (
    Flask,
    redirect,
    url_for,
    session
)

app = Flask(__name__)
app.secret_key = 'improper secret key'

@app.route('/')
def index():
    if not session.get('test', False):
        session['test'] = True
        return redirect(url_for('index'))
    else:
        return "Hello"

@app.route('/redirection/')
def redirection():
    # do something — login user, for example
    return redirect(url_for('index'))

test_redirect.py

from unittest import TestCase
from app import app

class RedirectTestCase(TestCase):

def setUp(self):
    self.test_client = app.test_client()

def testLoop(self):
    response = self.test_client.get('/redirection/', follow_redirects=True)
    self.assertTrue('Hello'.encode('ascii') in response.data)

Now, if I will run the test — it'll throw a ClientRedirectError: loop detected (even though, it could be seen from the code that second redirect will happen only once).

If I just run the app and go to the /redirection/ — it takes me to the index (i.e. /) with no problem, and no looping is happening.

The reason I need if not session.get('test', False): in index(), is because in my app I'm using it to set some things in session, in case user accessing / for the first time. As suggested by comment in code, in my 'real' app redirection() is a function that logs user in.


My questions are:

  1. Is there a 'right' way to overcome throwing of ClientRedirectError: loop detected in similar cases (i.e. is it possible to make the test run & pass)?
  2. Is there a better/'more correct' way to setup things in session for the 'first-time' user?
  3. Can mentioned behaviour be considered a bug in werkzeug (i.e. actual looping is not happening, but ClientRedirectError: loop detected is thrown, still)?

Workaround, I have came up with (which does not answer my questions, still):

def testLoop(self):
    self.test_client.get('/redirection/') # removed follow_redirects=True
    response = self.test_client.get('/', follow_redirects=True) # navigating to '/', directly 
    self.assertTrue('Hello'.encode('ascii') in response.data)

This might look redundant, but it's just a simplified example (it'll, probably, make more sense if self.test_client.get('/redirection/') would be replaced with something like self.test_client.post('/login/', data=dict(username='user', password='pass')).

  • To address questions #1 and #3, see the [Flask documentation on testing sessions](http://flask.pocoo.org/docs/0.12/testing/#accessing-and-modifying-sessions). Question #2 is not really suited to StackOverflow. – Chapman Atwell Nov 30 '17 at 23:32
  • @ChapmanAtwell, I have gone through Flask documentation previously (and looked it through, one more time, before writing this comment, just in case =). It does not mention redirection loops, at all (leave alone [werkzeug](http://werkzeug.pocoo.org/) implementation details), moreover the only section mentioning redirects (tangentially, in fact) is [Logging In and Out] (http://flask.pocoo.org/docs/0.12/testing/#logging-in-and-out). I would argue that question #2 is well suited to post on StackOverflow as it asks about best practices, in particular programming problem. –  Dec 01 '17 at 04:19
  • @ChapmanAtwell I also didn't get, how redirection loop is related to sessions, directly. It just happened so, that, in my case, `session` is involved in triggering (or not triggering) the redirect — there are, probably, other ways to do it, with the same result. Am I missing something? –  Dec 01 '17 at 04:41

1 Answers1

0

It seems, I am ready now (almost 1.5 yeast after posting them) to answer my own questions:

  1. It is possible to make test run & pass.
    Given the code, as it is above, the workaround, provided at the end of the question will do, though it can be shortened to:

    def testLoop(self):
        response = self.test_client.get('/', follow_redirects=True) # navigating to '/', directly 
        self.assertTrue('Hello'.encode('ascii') in response.data)
    

    in this particular case, there is no need for the self.test_client.get('/redirection/'), line. Though, if some changes were made to session['test'] inside the /redirection/, like so:

    @app.route('/redirection/')
    def redirection():
        session['test'] = True
        return redirect(url_for('index'))
    

    then, self.test_client.get('/redirection/'), would be necessary, so test would become:

    from unittest import TestCase
    from app import app
    
    class RedirectTestCase(TestCase):
    
    def setUp(self):
        self.test_client = app.test_client()
    
    def testLoop(self):
        self.test_client.get('/redirection/')
        response = self.test_client.get('/', follow_redirects=True)
    
        self.assertTrue('Hello'.encode('ascii') in response.data)
    

    This, so called, "workaround" is just preventing test from doing two redirects to /, in a row (which is exactly what is causing ClientRedirectError: loop detected — see answer to point 3, below).

  2. If there is a better way or not — will depend on the exact details of the app implementation. In the example from the question, the redirection inside index is, actually, unnecessary and can be removed:

    @app.route('/')
    def index():
        session['test'] = session.get('test', True)
        return "Hello"
    

    Such change, by the way, will also make the initial test (from the question) pass.

  3. As far as I can tell, such behaviour is "by design", namely: ClientRedirectError: loop detected is raised, when redirection to the same url occurs (at least) two times in a row. Below are some examples.

    This, test won't encounter ClientRedirectError (as redirection only happens once):

    isolated_app.py

    from flask import (
        Flask,
        redirect,
        url_for,
        session
        )
    
    app = Flask(__name__)
    app.secret_key = 'improper secret key'
    
    @app.route('/')
    def index():
        session['test'] = session.get('test', 0)
        if session['test'] < 1:
            session['test'] += 1
            return redirect(url_for('index'))
        else:
            return "Hello"
    

    test.py

    from unittest import TestCase
    from isolated_app import app
    
    class RedirectTestCase(TestCase):
    
    def setUp(self):
        self.test_client = app.test_client()
    
    def testLoop(self):
        response = self.test_client.get('/', follow_redirects=True)
        self.assertTrue('Hello'.encode('ascii') in response.data)
    

    Though, if the code of the app will be changed to:

    from flask import (
        Flask,
        redirect,
        url_for,
        session
        )
    
    app = Flask(__name__)
    app.secret_key = 'improper secret key'
    
    @app.route('/')
    def index():
        session['test'] = session.get('test', 0)
        if session['test'] < 1:
            session['test'] += 1
            return redirect(url_for('index'))
        elif session['test'] < 2:
            session['test'] += 1
            return redirect(url_for('index'))
        else:
            return "Hello"
    

    then test will fail, throwing werkzeug.test.ClientRedirectError: loop detected.

    It is also worth mentioning, that triggering of redirection loop errors is different for Flask tests and for real-life browsing. If we will run the last app, and go to / in browser — it will readily return Hello to us. Amount of allowed redirects is defined by each browser's developer (for example in FireFox it is defined by network.http.redirection-limit preference from about:config, and 20 is a default value). Here is an illustration:

    from flask import (
        Flask,
        redirect,
        url_for,
        session
        )
    
    app = Flask(__name__)
    app.secret_key = 'improper secret key'
    
    @app.route('/')
    def index():
        session['test'] = session.get('test', 0)
        while True:
            session['test'] = session.get('test', 0)
            session['test'] += 1
            print(session['test'])
            return redirect(url_for('index'))
    

    This will print 1 2, and then fail with werkzeug.test.ClientRedirectError: loop detected, if we will try to run the test for it. On the other hand, if we will run this app and go to / in FireFox — browser will show us The page isn’t redirecting properly message, and app will print 1 2 3 4 5 6 ... 21, in console.