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.
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