pythonroutescherrypy

Why is _cp_dispatch not routing to another method?


I have this CherryPy object to process some simple REST API, but I fail to understand how _cp_dispatch is supposed to work.

class Products(object):
    def __init__(self, product_database):
        self.product_database = product_database

    def _cp_dispatch(self, vpath):
        # batch/
        if len(vpath) == 1 and vpath[0] == "batch":
            vpath[0] = "realbatch"
            return self

        # batch/{id}/product/
        if len(vpath) == 3 and vpath[0] == "batch":            
            vpath.pop(0)        # pop "batch"
            cherrypy.request.params['batch_id'] = vpath.pop(0)
            return self
        
        # batch/{id}/product/{sku}/
        if len(vpath) == 4 and vpath[0] == "batch" and vpath[2] == "product":
            vpath.pop(0)        # pop "batch"
            cherrypy.request.params['batch_id'] = vpath.pop(0)
            vpath.pop(0)        # pop "product"
            cherrypy.request.params['sku'] = vpath[0]
            vpath[0] = "product"
            return self
        
        # defaults
        return self

    # POST   rest-products/batch      --> create a batch (name in JSON body), return batch id
    # GET    rest-products/batch      --> return list of batches
    @cherrypy.expose
    @cherrypy.tools.json_out()
    def realbatch(self):
        # this method is not called when calling "batch/"
        pass

    # #  POST   rest-products/batch/{id}/product --> add product to batch (product data in JSON body)
    @cherrypy.expose
    @cherrypy.tools.json_out()
    def product(self, batch_id, sku = None):
        # this method is called when calling "batch/{id}/product/" or "batch/{id}/product/{sku}
        pass

As how I understand it, CherryPy tries to find a match on the path (which fails, because it cannot find a batch method). Then it tries to find a catch-all index() which also fails, so it goes to _cp_dispatch. In my case, for batch/ to rewrites it into realbatch/, and tries again. However, that doesn't happen.

I can fix it by simply renaming realbatch to index, but then calls to batc/ work as well, which I don't want.

My goal is simple: route batch/ to the realbatch() method and batch/{id}/product/{sku} to product(). All other paths should return 404.


Solution

  • Using _cp_dispatch in CherryPy for Custom REST Routing

    CherryPy allows you to override how URL paths are mapped to methods using the special _cp_dispatch(self, vpath) method.

    CherryPy first tries to match the URL path (/foo/bar) to exposed methods (@cherrypy.expose).

    If it cannot, it calls _cp_dispatch with the remaining path segments (vpath).

    Your job inside _cp_dispatch is to either:

    Mutate vpath and return self (to continue traversal), or

    Return a specific method (e.g., getattr(self, "mymethod")).

    If _cp_dispatch returns None, CherryPy will raise 404.

    Problem ?

    In your case

    You wanted /batch/ → realbatch()

    /batch/{id}/product/ → product(batch_id=…)

    /batch/{id}/product/{sku} → product(batch_id=…, sku=…)

    All other paths → 404

    But realbatch() was never called, because rewriting "batch" → "realbatch" in vpath does not automatically map to the method. CherryPy treats it as a sub-object.

    Solution

    The cleaner way is to directly return the handler methods from _cp_dispatch instead of trying to rewrite vpath.

    Code -

    import cherrypy
    class Products(object):
        def __init__(self, product_database):
            self.product_database = product_database
    
        def _cp_dispatch(self, vpath):
            # /batch/
            if len(vpath) == 1 and vpath[0] == "batch":
                return getattr(self, "realbatch")
    
            # /batch/{id}/product/
            if len(vpath) == 3 and vpath[0] == "batch":
                vpath.pop(0)  # remove "batch"
                cherrypy.request.params['batch_id'] = vpath.pop(0)
                return getattr(self, "product")
    
            # /batch/{id}/product/{sku}/
            if len(vpath) == 4 and vpath[0] == "batch" and vpath[2] == "product":
                vpath.pop(0)  # remove "batch"
                cherrypy.request.params['batch_id'] = vpath.pop(0)
                vpath.pop(0)  # remove "product"
                cherrypy.request.params['sku'] = vpath.pop(0)
                return getattr(self, "product")
    
            # No match → 404
            return None
    
        @cherrypy.expose
        @cherrypy.tools.json_out()
        def realbatch(self):
            return {"msg": "batch list or create batch here"}
    
        @cherrypy.expose
        @cherrypy.tools.json_out()
        def product(self, batch_id, sku=None):
            return {"batch": batch_id, "sku": sku}
    
    if __name__ == "__main__":
        cherrypy.quickstart(Products({}), "/rest-products")
    

    --------------

    Behavior

    GET /rest-products/batch/ → calls realbatch()

    POST /rest-products/batch/ → also calls realbatch()

    POST /rest-products/batch/123/product/ → calls product(batch_id=123)

    GET /rest-products/batch/123/product/ABC123 → calls product(batch_id=123, sku="ABC123")

    Anything else → 404