ploneasgi.zcml#

A ZCML directive for registering native ASGI routes.

An add-on cannot edit the project's server.py, so there has to be some way for it to contribute a websocket endpoint or a native HTTP route. In this ecosystem that mechanism is ZCML, and this module supplies one directive:

<configure xmlns:ploneasgi="http://namespaces.plone.org/ploneasgi">
  <ploneasgi:route
      path="/notify"
      handler=".endpoints.notify"
      type="websocket"
      permission="cmf.ManagePortal"
      />
</configure>

Paths are relative to a reserved prefix (/++asgi++ by default), so that one lands at /++asgi++/notify. That is not cosmetic: it is what keeps a deployment to a single proxy rule. ploneasgi.vhm.VirtualHostMiddleware recognizes the prefix inside an ordinary VirtualHostMonster rewrite, so registering a new route needs no infrastructure change — and the Plone site comes out of the rewrite rather than being restated in every path.

Two deliberate limits, both consequences of how ZCML actually works:

  • Registrations are process-wide, not per-site. ZCML executes once at startup into the global registry; local (persistent) registrations live in the ZODB and can change at runtime, so a startup snapshot of them would go stale the moment an add-on is installed. Which site a request enters is resolved per request from the rewrite, so one registration serves every site.

  • Discovery is explicit. routes_from_registry() is called by the project, not by ploneasgi.app.create_app(). "Adding a route means passing a route" still holds; this just lets an add-on put one in the pile.

The permission attribute is the reason this is worth having over a plain list of routes. It is checked against the portal, inside ploneasgi.context.run_authenticated(), before the handler is entered — so an endpoint cannot forget to authorize itself.

The two route types run differently, because they last differently:

  • An http handler is a sync function, called in the worker thread inside the same context the permission was checked in:

    def whoami(ctx: PloneContext, request: Request) -> Response:
        return JSONResponse({"user": ctx.user.getUserName()})
    

    One thread hop, one ZODB connection, one authentication per request. A StreamingResponse or FileResponse still sends its body on the event loop, after the thread is released.

  • A websocket handler is async def handler(websocket). A session lasts as long as the client stays, and must not hold a worker thread; the check runs in a call of its own before the socket is accepted.

ploneasgi.zcml.WS_POLICY_VIOLATION = 1008#

Websocket close code for a policy violation (RFC 6455). Used when a connection is refused for lack of a permission.

class ploneasgi.zcml.ASGIRoute[source]#

One registered route, as declared in ZCML.

Variables:
  • path -- Path pattern, relative to the reserved prefix.

  • handler -- The endpoint callable: handler(ctx, request) returning a Response for http, async handler(websocket) for websocket.

  • type -- http or websocket.

  • methods -- HTTP methods; ignored for websocket routes.

  • permission -- Id of the permission required on the portal, or None.

  • site -- Fallback site id for requests arriving with no VHM rewrite.

  • name -- Route name.

__init__(path, handler, type='http', methods=<factory>, permission=None, site=None, name='')#
Parameters:
Return type:

None

ploneasgi.zcml.build_route(route, prefix='/++asgi++')[source]#

Turn a registration into a Starlette route.

Declared paths are relative to the reserved prefix. Mounting every native route under one prefix is what lets a deployment keep a single proxy rule: ploneasgi.vhm.VirtualHostMiddleware recognizes the prefix inside an ordinary VirtualHostMonster rewrite, so adding a route needs no proxy change at all.

Parameters:
  • route (ASGIRoute) -- The registered route.

  • prefix (str) -- Reserved path prefix.

Returns:

A Route or WebSocketRoute.

Return type:

BaseRoute

ploneasgi.zcml.routes_from_registry(registry=None, prefix='/++asgi++')[source]#

Collect every ZCML-registered route as a Starlette route.

Call this after Zope has started — ZCML has not executed before that — and pass the result to ploneasgi.app.create_app():

wsgi_app = build_wsgi_app(settings)
app = create_app(settings, routes=routes_from_registry(), wsgi_app=wsgi_app)

Ordering is by type and path so the route table is deterministic across restarts; Starlette matches in list order, and a non-deterministic order would make an overlapping pattern win at random.

Parameters:
  • registry (Any) -- Component registry to read; defaults to the global one.

  • prefix (str) -- Reserved path prefix to mount the routes under.

Returns:

The routes, ready to pass to create_app.

Return type:

list[BaseRoute]

ploneasgi.zcml.routeDirective(_context, path, handler, type='http', methods='GET', permission=None, site=None, name='', unguarded=False)[source]#

Register one ASGI route from ZCML.

Parameters:
  • _context (Any) -- The ZCML configuration context.

  • path (str) -- Starlette path pattern.

  • handler (Callable[[...], Any]) -- The endpoint callable, already resolved by GlobalObject.

  • type (str) -- http or websocket.

  • methods (str) -- Space-separated HTTP methods.

  • permission (str | None) -- Permission required on the portal.

  • site (str | None) -- Fallback site id for requests with no VHM rewrite.

  • name (str) -- Route name.

  • unguarded (bool) -- Required when permission is omitted.

Raises:

ValueError -- On an unknown type, a missing site, or an unacknowledged missing permission.

Return type:

None