Commits

Robert Brewer committed 4d848dc

InternalRedirect changes:

1. Moved InternalRedirect out of the Request object, so that an IR creates a separate Request object per redirect. This makes the design of hooks and tools (both builtin and user-defined) much simpler and safer.
2. New _cpwsgi.InternalRedirector for the WSGI implementation. Users who do not use InternalRedirects at all can remove this from the wsgi pipeline if they wish.
3. InternalRedirect trap implemented in _cpmodpy.py
4. Custom servers/gateways will have to write their own handling for InternalRedirect being thrown from request.run.
5. Request.redirections (a list of URL's) removed in favor of request.prev, which points to the previous Request object in a redirection chain. Defaults to None.
6. Replace "request.recursive" (per request) with "wsgi.iredir.recursive" (per app).
7. Removed Request.body_read and .headers_read.
8. New Request.throws tuple of exceptions which should not be trapped.

Comments (0)

Files changed (5)

cherrypy/_cperror.py

 class CherryPyException(exceptions.Exception):
     pass
 
+
 class InternalRedirect(CherryPyException):
     """Exception raised to switch to the handler for a different URL.
     
-    If you supply a query string, it will replace request.params.
-    If you omit the query string (no question mark), the params from the
-    original request will remain in effect.
-    To erase any query string parameters, write url + "?" with no params.
+    Any request.params must be supplied in a query string.
     """
     
     def __init__(self, path):

cherrypy/_cpmodpy.py

 Then restart apache2 and access http://localhost:8080
 """
 
+import StringIO
+
 import cherrypy
 from cherrypy._cperror import format_exc, bare_error
 from cherrypy.lib import http
             self.__dict__[method] = getattr(req, method)
 
 
+recursive = False
+
 _isSetUp = False
 def handler(req):
     from mod_python import apache
         remote = http.Host(remote[0], remote[1], req.connection.remote_host or "")
         
         scheme = req.parsed_uri[0] or 'http'
-        request = cherrypy.engine.request(local, remote, scheme)
         req.get_basic_auth_pw()
-        request.login = req.user
         
         try:
             # apache.mpm_query only became available in mod_python 3.1
                 forked = False
             else:
                 raise ValueError(bad_value % "multiprocess")
-        request.multithread = bool(threaded)
-        request.multiprocess = bool(forked)
         
         sn = cherrypy.tree.script_name(req.uri or "/")
         if sn is None:
             send_response(req, '404 Not Found', [], '')
         else:
-            request.app = cherrypy.tree.apps[sn]
-            
-            # Run the CherryPy Request object and obtain the response
+            app = cherrypy.tree.apps[sn]
+            method = req.method
+            path = req.uri
+            qs = req.args or ""
+            sproto = req.protocol
             headers = req.headers_in.items()
             rfile = _ReadOnlyRequest(req)
-            response = request.run(req.method, req.uri, req.args or "",
-                                   req.protocol, headers, rfile)
+            prev = None
+            
+            redirections = []
+            while True:
+                request = cherrypy.engine.request(local, remote, scheme)
+                request.login = req.user
+                request.multithread = bool(threaded)
+                request.multiprocess = bool(forked)
+                request.app = app
+                request.prev = prev
+                
+                # Run the CherryPy Request object and obtain the response
+                try:
+                    response = request.run(method, path, qs, sproto, headers, rfile)
+                    break
+                except cherrypy.InternalRedirect, ir:
+                    request.close()
+                    prev = request
+                    
+                    if not recursive:
+                        if ir.path in redirections:
+                            raise RuntimeError("InternalRedirector visited the "
+                                               "same URL twice: %r" % ir.path)
+                        else:
+                            # Add the *previous* path_info + qs to redirections.
+                            if qs:
+                                qs = "?" + qs
+                            redirections.append(sn + path + qs)
+                    
+                    # Munge environment and try again.
+                    method = "GET"
+                    path = ir.path
+                    qs = ir.query_string
+                    rfile = StringIO.StringIO()
             
             send_response(req, response.status, response.header_list, response.body)
             request.close()

cherrypy/_cprequest.py

 class Request(object):
     """An HTTP request."""
     
+    prev = None
+    
     # Conversation/connection attributes
     local = http.Host("localhost", 80)
     remote = http.Host("localhost", 1111)
     process_request_body = True
     methods_with_bodies = ("POST", "PUT")
     body = None
-    body_read = False
-    headers_read = False
     
     # Dispatch attributes
     dispatch = Dispatcher()
     handler = None
     toolmaps = {}
     config = None
-    recursive_redirect = False
     is_index = None
     
     hooks = HookMap(hookpoints)
     error_response = cherrypy.HTTPError(500).set_response
     error_page = {}
     show_tracebacks = True
+    throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
     throw_errors = False
     
     namespaces = {"hooks": hooks_namespace,
         self.server_protocol = server_protocol
         
         self.closed = False
-        self.redirections = []
         
         # Put a *copy* of the class error_page into self.
         self.error_page = self.error_page.copy()
             # path_info should be the path from the
             # app root (script_name) to the handler.
             self.script_name = self.app.script_name
-            self.path_info = path[len(self.script_name.rstrip("/")):]
+            self.path_info = pi = path[len(self.script_name.rstrip("/")):]
             
-            # Loop to allow for InternalRedirect.
-            pi = self.path_info
-            qs = self.query_string
-            while True:
-                try:
-                    self.respond(pi)
-                    break
-                except cherrypy.InternalRedirect, ir:
-                    if (ir.path in self.redirections
-                        and not self.recursive_redirect):
-                        raise RuntimeError("InternalRedirect visited the "
-                                           "same URL twice: %s" % repr(ir.path))
-                    
-                    # Add the *previous* path_info + qs to self.redirections.
-                    if qs:
-                        qs = "?" + qs
-                    self.redirections.append(pi + qs)
-                    
-                    pi = self.path_info = ir.path
-                    qs = self.query_string = ir.query_string
-                    if qs:
-                        self.params = http.parse_query_string(qs)
-        except (KeyboardInterrupt, SystemExit):
+            self.respond(pi)
+            
+        except self.throws:
             raise
         except:
             if self.throw_errors:
                 raise
             else:
+                # Failure in setup, error handler or finalize. Bypass them.
                 # Can't use handle_error because we may not have hooks yet.
                 cherrypy.log(traceback=True)
-                
-                # Failure in setup, error handler or finalize. Bypass them.
                 if self.show_tracebacks:
                     body = format_exc()
                 else:
                     if self.app is None:
                         raise cherrypy.NotFound()
                     
-                    if not self.headers_read:
-                        # Get the 'Host' header, so we can do HTTPRedirects properly.
-                        self.process_headers()
+                    # Get the 'Host' header, so we can do HTTPRedirects properly.
+                    self.process_headers()
                     
                     # Make a copy of the class hooks
                     self.hooks = self.__class__.hooks.copy()
                     
                     self.hooks.run('on_start_resource')
                     
-                    if not self.body_read:
-                        if self.process_request_body:
-                            if self.method not in self.methods_with_bodies:
-                                self.process_request_body = False
+                    if self.process_request_body:
+                        if self.method not in self.methods_with_bodies:
+                            self.process_request_body = False
                         
                         if self.process_request_body:
                             # Prepare the SizeCheckWrapper for the request body
                     cherrypy.response.finalize()
             finally:
                 self.hooks.run('on_end_resource')
-        except (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect):
+        except self.throws:
             raise
         except:
             if self.throw_errors:
             self.handle_error(sys.exc_info())
     
     def process_headers(self):
-        # Guard against re-reading body (e.g. on InternalRedirect)
-        if self.headers_read:
-            return
-        self.headers_read = True
-        
         self.params = http.parse_query_string(self.query_string)
         
         # Process the headers into self.headers
     
     def process_body(self):
         """Convert request.rfile into request.params (or request.body)."""
-        # Guard against re-reading body (e.g. on InternalRedirect)
-        if self.body_read:
-            return
-        self.body_read = True
-        
         # FieldStorage only recognizes POST, so fake it.
         methenv = {'REQUEST_METHOD': "POST"}
         try:

cherrypy/_cpwsgi.py

 """WSGI interface (see PEP 333)."""
 
+import StringIO as _StringIO
 import sys as _sys
 
 import cherrypy as _cherrypy
 from cherrypy.lib import http as _http
 
 
+class InternalRedirector(object):
+    """WSGI middleware which handles cherrypy.InternalRedirect.
+    
+    When cherrypy.InternalRedirect is raised, this middleware traps it,
+    rewrites the WSGI environ using the new path and query_string,
+    and calls the next application again. Because the wsgi.input stream
+    may have already been consumed by the next application, the redirected
+    call will always be of HTTP method "GET", and therefore any params must
+    be passed in the InternalRedirect object's query_string attribute.
+    If you need something more complicated, make and raise your own
+    exception and your own WSGI middlewre to trap it. ;)
+    
+    It would be a bad idea to raise InternalRedirect after you've already
+    yielded response content, although an enterprising soul could choose
+    to abuse this.
+    
+    nextapp: the next application callable in the WSGI chain.
+    
+    recursive: if False (the default), each URL (path + qs) will be
+    stored, and, if the same URL is requested again, RuntimeError will
+    be raised. If 'recursive' is True, no such error will be raised.
+    """
+    
+    def __init__(self, nextapp, recursive=False):
+        self.nextapp = nextapp
+        self.recursive = recursive
+    
+    def __call__(self, environ, start_response):
+        redirections = []
+        
+        env = environ.copy()
+        path = env.get('PATH_INFO', '')
+        qs = env.get('QUERY_STRING', '')
+        
+        while True:
+            try:
+                for chunk in self.nextapp(env, start_response):
+                    yield chunk
+                break
+            except _cherrypy.InternalRedirect, ir:
+                if not self.recursive:
+                    if ir.path in redirections:
+                        raise RuntimeError("InternalRedirector visited the "
+                                           "same URL twice: %r" % ir.path)
+                    else:
+                        # Add the *previous* path_info + qs to redirections.
+                        if qs:
+                            qs = "?" + qs
+                        redirections.append(env.get('SCRIPT_NAME', '') + path + qs)
+                
+                # Munge environment and try again.
+                env['REQUEST_METHOD'] = "GET"
+                env['PATH_INFO'] = path = ir.path
+                env['QUERY_STRING'] = qs = ir.query_string
+                env['wsgi.input'] = _StringIO.StringIO()
+
+
 class CPWSGIApp(object):
     """A WSGI application object for a CherryPy Application.
     
         named WSGI callable (from the pipeline) as keyword arguments.
     """
     
-    pipeline = []
+    pipeline = [('iredir', InternalRedirector)]
     head = None
     config = {}
     
-    throws = (KeyboardInterrupt, SystemExit)
+    throws = (KeyboardInterrupt, SystemExit, _cherrypy.InternalRedirect)
     
     headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
                    'CONTENT_LENGTH': 'Content-Length',
         request.multiprocess = environ['wsgi.multiprocess']
         request.wsgi_environ = environ
         request.app = self.cpapp
+        request.prev = env('cherrypy.request')
+        environ['cherrypy.request'] = request
         return request
     
     def tail(self, environ, start_response):

cherrypy/test/test_core.py

             raise cherrypy.InternalRedirect("cousin?t=6")
         
         def cousin(self, t):
-            return repr(cherrypy.request.redirections +
-                        [cherrypy.url(qs=cherrypy.request.query_string)])
+            assert cherrypy.request.prev.closed
+            return cherrypy.request.prev.query_string
         
         def petshop(self, user_id):
             if user_id == "parrot":
                 raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=slug')
             elif user_id == "terrier":
                 # Trade it for a fish when redirecting
-                cherrypy.request.params = {"user_id": "fish"}
-                raise cherrypy.InternalRedirect('/image/getImagesByUser')
+                raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=fish')
             else:
                 # This should pass the user_id through to getImagesByUser
-                raise cherrypy.InternalRedirect('/image/getImagesByUser')
+                raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=%s' % user_id)
         
         # We support Python 2.3, but the @-deco syntax would look like this:
         # @tools.login_redir()
         
         def custom_err(self):
             return "Something went horribly wrong."
+        
+        def early_ir(self, arg):
+            return "whatever"
+        early_ir._cp_config = {'hooks.before_request_body': redir_custom}
     
     class Image(Test):
         
         self.assertBody('')
         self.assertStatus(305)
         
-        # InternalRedirect
-        self.getPage("/internalredirect/")
-        self.assertBody('hello')
-        self.assertStatus(200)
-        
-        self.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film")
-        self.assertBody('0 images for Sir-not-appearing-in-this-film')
-        self.assertStatus(200)
-        
-        self.getPage("/internalredirect/petshop?user_id=parrot")
-        self.assertBody('0 images for slug')
-        self.assertStatus(200)
-        
-        self.getPage("/internalredirect/petshop?user_id=terrier")
-        self.assertBody('0 images for fish')
-        self.assertStatus(200)
-        
-        self.getPage("/internalredirect/secure")
-        self.assertBody('Please log in')
-        self.assertStatus(200)
-        
-        # Relative path in InternalRedirect.
-        # Also tests request.redirections
-        self.getPage("/internalredirect/relative?a=3&b=5")
-        self.assertBody("['/internalredirect/relative?a=3&b=5', "
-                        "'%s/internalredirect/cousin?t=6']" % self.base())
-        self.assertStatus(200)
-        
-        # InternalRedirect on error
-        self.getPage("/internalredirect/login/illegal/extra/vpath/atoms")
-        self.assertStatus(200)
-        self.assertBody("Something went horribly wrong.")
-        
         # HTTPRedirect on error
         self.getPage("/redirect/error/")
         self.assertStatus(('302 Found', '303 See Other'))
             self.assertStatus(200)
             self.assertBody("(['%s/'], 303)" % self.base())
     
+    def test_InternalRedirect(self):
+        # InternalRedirect
+        self.getPage("/internalredirect/")
+        self.assertBody('hello')
+        self.assertStatus(200)
+        
+        # Test passthrough
+        self.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film")
+        self.assertBody('0 images for Sir-not-appearing-in-this-film')
+        self.assertStatus(200)
+        
+        # Test args
+        self.getPage("/internalredirect/petshop?user_id=parrot")
+        self.assertBody('0 images for slug')
+        self.assertStatus(200)
+        
+        # Test POST
+        self.getPage("/internalredirect/petshop", method="POST",
+                     body="user_id=terrier")
+        self.assertBody('0 images for fish')
+        self.assertStatus(200)
+        
+        # Test ir before body read
+        self.getPage("/internalredirect/early_ir", method="POST",
+                     body="arg=aha!")
+        self.assertBody("Something went horribly wrong.")
+        self.assertStatus(200)
+        
+        self.getPage("/internalredirect/secure")
+        self.assertBody('Please log in')
+        self.assertStatus(200)
+        
+        # Relative path in InternalRedirect.
+        # Also tests request.prev.
+        self.getPage("/internalredirect/relative?a=3&b=5")
+        self.assertBody("a=3&b=5")
+        self.assertStatus(200)
+        
+        # InternalRedirect on error
+        self.getPage("/internalredirect/login/illegal/extra/vpath/atoms")
+        self.assertStatus(200)
+        self.assertBody("Something went horribly wrong.")
+    
     def testFlatten(self):
         for url in ["/flatten/as_string", "/flatten/as_list",
                     "/flatten/as_yield", "/flatten/as_dblyield",
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.