This one has been torturing me for a while. Here is how to replicate it consistently (and possibly fix it):
1. Create a brand new application (in my case named 'ttt') 2. In db.py say "auth.settings.expiration=10" (so sessions expire quick) 3. In the default controller create a new function: @auth.requires_login() def frm(): form = SQLFORM.factory( Field('email', requires= IS_EMAIL(error_message=auth.messages.invalid_email))) return dict(form=form) 4. Register a user (in my case i...@gmail.com) 5. Login with your new user and visit http://localhost/ttt/default/frm/ 6. Let session expire (wait 10s) 7. In the form 'email' field enter 'i...@gmail.com' and hit 'Submit' 8. You get: Traceback (most recent call last): File "/pub/web2py/gluon/restricted.py", line 194, in restricted exec ccode in environment File "/pub/web2py/applications/ttt/controllers/default.py", line 75, in <module> File "/pub/web2py/gluon/globals.py", line 149, in <lambda> self._caller = lambda f: f() File "/pub/web2py/applications/ttt/controllers/default.py", line 37, in user return dict(form=auth()) File "/pub/web2py/gluon/tools.py", line 1126, in __call__ return getattr(self,args[0])() File "/pub/web2py/gluon/tools.py", line 1726, in login next = replace_id(next, form) File "/pub/web2py/gluon/tools.py", line 79, in replace_id return url % form.vars TypeError: float argument required, not Storage So it tries to get back to: http://localhost/ttt/default/user/login?_next=/ttt/default/frm%3Femail%3Diii%2540gmail.com Inside replace_id(...): url is '/ttt/default/frm?email=iii%40gmail.com' form.vars is Storage({'password': '5638e85e6f35b0716e5ddac33a0', 'email': 'i...@gmail.com', 'remember': None}) Also try from the python console: '/ttt/default/frm?email=iii%40gmail.com' % Storage({'password': '5638e85e6f35b0716e5ddac33a0', 'email': 'i...@gmail.com', 'remember': None}) Like Jim noted in the original post the '%' containing, URL escaped symbols in 'url' coincide with Python's interpretation for a float. Notice I don't have %2F in my string, here it looks like the '%40g' is doing it. A patch which seems to be fixing it is: diff gluon/tools.py gluon/tools.py.patch 79c79 < return url % form.vars --- > return urllib.unquote(url) % form.vars but looking at the comment on gluon/tools.py:78 "this allows http://..../%(id)s/%(name)s/etc." I have not tested with such URLs. Running 1.99.2, BTW. Let me know if I can help further...