简体   繁体   中英

How to avoid “ClientRedirectError: loop detected” error during testing Flask application (and how is it triggered)?

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 (ie 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 (ie / ) 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 (ie 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 (ie 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')) .

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM