Environment:
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:
ClientRedirectError: loop detected
in similar cases (i.e. is it possible to make the test run & pass)?session
for the 'first-time' user?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'))
.
It seems, I am ready now (almost 1.5 yeast after posting them) to answer my own questions:
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).
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.
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.